From c93ace77c278b26ebcc1485dba2c4c0719eaa721 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 3 Sep 2026 14:00:17 -0700 Subject: [PATCH] =?UTF-8?q?simplify(compat):=20config/runtime=5Fprovider/p?= =?UTF-8?q?lugins/commands/secrets=5Fcli/kanban=20=E2=80=94=20drop=2096=20?= =?UTF-8?q?re-exports=20(incl.=20PEP=20562=20facades)=20+=203=20aliases=20?= =?UTF-8?q?(get=5Fpre=5Ftool=5Fcall=5Fdirective/=5Fblock=5Fmessage,=20get?= =?UTF-8?q?=5Ftelegram=5Fhandler=5Ffactories),=20repoint=2056=20callers=20?= =?UTF-8?q?+=2050=20test=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- acp_adapter/model_catalog.py | 3 +- agent/agent_init.py | 14 ++--- agent/agent_runtime_helpers.py | 19 ++++--- agent/auxiliary_client.py | 7 ++- agent/client_lifecycle.py | 9 +-- agent/credential_pool.py | 2 +- agent/model_metadata.py | 7 ++- agent/system_prompt.py | 8 +-- cli.py | 2 +- gateway/relay/ws_transport.py | 2 +- gateway/run.py | 6 +- gateway/run_config_loaders.py | 5 +- gateway/run_inbound.py | 4 +- gateway/run_turn.py | 4 +- gateway/slash_commands_model.py | 2 +- hermes_cli/auth_commands.py | 3 +- hermes_cli/cli_model_switch_mixin.py | 2 +- hermes_cli/cli_tui_mixin.py | 2 +- hermes_cli/commands.py | 23 -------- hermes_cli/commands_completion.py | 2 +- hermes_cli/commands_platforms.py | 8 +-- hermes_cli/config.py | 18 +----- hermes_cli/config_migrations.py | 2 +- hermes_cli/config_providers.py | 4 +- hermes_cli/doctor_config.py | 4 +- hermes_cli/inventory.py | 5 +- hermes_cli/kanban.py | 4 +- hermes_cli/main.py | 3 +- hermes_cli/main_provider_setup.py | 3 +- hermes_cli/model_setup_flows_custom.py | 2 +- hermes_cli/model_switch.py | 6 +- hermes_cli/model_switch_providers.py | 8 +-- hermes_cli/models.py | 4 +- hermes_cli/models_local.py | 2 +- hermes_cli/plugin_dev.py | 4 +- hermes_cli/plugins.py | 45 +++------------ hermes_cli/runtime_provider.py | 35 +++++------- hermes_cli/runtime_provider_backends.py | 6 +- hermes_cli/runtime_provider_custom.py | 15 ++--- hermes_cli/secrets_cli.py | 14 ----- hermes_cli/slack_cli.py | 4 +- hermes_cli/web_routers/config_env.py | 5 +- hermes_cli/web_server_config.py | 12 ++-- hermes_cli/web_server_dashboard.py | 3 +- hermes_cli/web_server_profiles.py | 3 +- plugins/memory/__init__.py | 2 +- plugins/platforms/discord/adapter.py | 2 +- plugins/platforms/slack/adapter.py | 4 +- plugins/platforms/telegram/adapter.py | 4 +- plugins/platforms/telegram/inline_picker.py | 4 +- .../agent/test_auxiliary_client_ssl_verify.py | 3 +- .../test_auxiliary_named_custom_providers.py | 2 +- tests/agent/test_command_token_source.py | 3 +- tests/agent/test_custom_provider_ca_probes.py | 36 ++++++------ tests/agent/test_plan_prompt.py | 2 +- tests/cli/test_cli_provider_resolution.py | 2 +- tests/cli/test_resume_model_restore.py | 7 ++- tests/gateway/test_background_command.py | 2 +- tests/gateway/test_discord_slash_commands.py | 4 +- .../test_reload_skills_discord_resync.py | 4 +- tests/gateway/test_session_info.py | 2 +- tests/gateway/test_status_command.py | 2 +- tests/gateway/test_telegram_forum_commands.py | 4 +- ..._anthropic_oauth_routes_to_messages_api.py | 6 +- tests/hermes_cli/test_api_mode_aliases.py | 7 ++- tests/hermes_cli/test_approvals_command.py | 11 +--- .../test_at_context_completion_filter.py | 2 +- tests/hermes_cli/test_bitwarden_status.py | 9 ++- .../test_canonical_custom_identity.py | 41 ++++++------- tests/hermes_cli/test_commands.py | 51 ++++++----------- tests/hermes_cli/test_config.py | 27 ++------- .../test_custom_provider_context_length.py | 5 +- .../test_custom_provider_extra_headers.py | 8 +-- .../test_custom_provider_identity.py | 21 +++---- ...est_custom_provider_normalize_no_mutate.py | 6 +- tests/hermes_cli/test_custom_provider_tls.py | 5 +- .../test_discord_skill_clamp_warning.py | 4 +- .../test_keyed_provider_credential_pool.py | 2 +- tests/hermes_cli/test_meta_prompt_cache.py | 2 +- .../test_model_switch_custom_providers.py | 3 +- tests/hermes_cli/test_path_completion.py | 2 +- tests/hermes_cli/test_plugin_capabilities.py | 4 +- tests/hermes_cli/test_plugin_event_bus.py | 12 ++-- tests/hermes_cli/test_plugin_manifest_v2.py | 9 +-- .../hermes_cli/test_plugin_prompt_sections.py | 8 +-- tests/hermes_cli/test_plugins.py | 57 ++++++++----------- .../test_provider_config_validation.py | 7 +-- .../test_resolve_ephemeral_system_prompt.py | 15 ++--- .../test_runtime_provider_late_binding.py | 4 +- .../test_runtime_provider_resolution.py | 48 ++++++++-------- .../test_secrets_bitwarden_non_tty.py | 6 +- .../hermes_cli/test_secrets_token_rotation.py | 5 +- .../test_63425_credential_pool_auto_detect.py | 2 +- .../test_anthropic_prompt_cache_policy.py | 2 +- ...test_credential_rotation_route_settings.py | 2 +- .../test_env_credential_turn_refresh.py | 3 +- tests/run_agent/test_switch_model_context.py | 4 +- tests/test_tui_gateway_server.py | 4 +- tests/tools/test_cronjob_tools.py | 9 +-- ...est_custom_provider_session_persistence.py | 7 ++- tools/cronjob_job_args.py | 6 +- tui_gateway/methods_complete.py | 2 +- tui_gateway/methods_complete_helpers.py | 2 +- tui_gateway/model_switch.py | 3 +- tui_gateway/server.py | 12 ++-- tui_gateway/session_workdir.py | 2 +- 106 files changed, 371 insertions(+), 510 deletions(-) diff --git a/acp_adapter/model_catalog.py b/acp_adapter/model_catalog.py index c6ac6045d4..c114687e58 100644 --- a/acp_adapter/model_catalog.py +++ b/acp_adapter/model_catalog.py @@ -25,7 +25,8 @@ def _named_custom_provider_catalogs() -> list[tuple[str, str, list[tuple[str, st discovery (some endpoints have no ``/models`` route). Slugs use the ``custom:`` shape ``parse_model_input``/``resolve_runtime_provider`` resolve, so choice ids round-trip.""" try: - from hermes_cli.config import (get_compatible_custom_providers, is_provider_enabled, load_config) + from hermes_cli.config import load_config + from hermes_cli.config_providers import get_compatible_custom_providers, is_provider_enabled 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 diff --git a/agent/agent_init.py b/agent/agent_init.py index cb393b21cc..c1751c13f7 100644 --- a/agent/agent_init.py +++ b/agent/agent_init.py @@ -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 ( - apply_custom_provider_extra_headers_to_client_kwargs, - apply_custom_provider_tls_to_client_kwargs, get_compatible_custom_providers, - load_config, + 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 ) _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 import is_provider_enabled + from hermes_cli.config_providers 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 import get_compatible_custom_providers + from hermes_cli.config_providers 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 import get_custom_provider_context_length + from hermes_cli.config_providers 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 ) diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py index 613a9b03e7..df426b2462 100644 --- a/agent/agent_runtime_helpers.py +++ b/agent/agent_runtime_helpers.py @@ -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 import get_custom_provider_model_capability + from hermes_cli.config_providers 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,7 +1768,8 @@ 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, get_compatible_custom_providers + from hermes_cli.config import load_config + from hermes_cli.config_providers import get_compatible_custom_providers custom_providers = get_compatible_custom_providers(load_config()) except Exception: custom_providers = [] @@ -1891,9 +1892,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 ( - apply_custom_provider_tls_to_client_kwargs, get_compatible_custom_providers, - load_config_readonly, + 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 ) # Read live config, not agent._custom_providers, so mid-session ssl_ca_cert / ssl_verify # edits are honored. @@ -1962,9 +1963,8 @@ 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 ( - get_compatible_custom_providers, get_custom_provider_context_length, load_config - ) + from hermes_cli.config import load_config + from hermes_cli.config_providers import get_compatible_custom_providers, get_custom_provider_context_length 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,7 +1996,8 @@ 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 get_compatible_custom_providers, load_config + from hermes_cli.config import load_config + from hermes_cli.config_providers import get_compatible_custom_providers custom_providers = get_compatible_custom_providers(load_config()) except Exception: custom_providers = None diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 4172e4f515..7757c65af4 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -130,7 +130,8 @@ 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 get_custom_provider_tls_settings, load_config_readonly + from hermes_cli.config import load_config_readonly + from hermes_cli.config_providers import get_custom_provider_tls_settings 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 "")) @@ -4079,7 +4080,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 import _get_named_custom_provider + from hermes_cli.runtime_provider_custom 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:`` so the named arm honours the entry's api_mode @@ -4545,7 +4546,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 import _get_named_custom_provider + from hermes_cli.runtime_provider_custom 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 diff --git a/agent/client_lifecycle.py b/agent/client_lifecycle.py index 57384bb1c1..cf621c7b92 100644 --- a/agent/client_lifecycle.py +++ b/agent/client_lifecycle.py @@ -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 import _get_named_custom_provider + from hermes_cli.runtime_provider_custom 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 import apply_custom_provider_extra_headers_to_client_kwargs + from hermes_cli.config_providers 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,8 +836,9 @@ class ClientLifecycleMixin: self._client_kwargs.pop("ssl_verify", None) self._client_kwargs.pop("ssl_ca_cert", None) try: - from hermes_cli.config import ( - apply_custom_provider_tls_to_client_kwargs, get_compatible_custom_providers, load_config_readonly, + 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 ) apply_custom_provider_tls_to_client_kwargs( self._client_kwargs, str(self.base_url or ""), get_compatible_custom_providers(load_config_readonly()), diff --git a/agent/credential_pool.py b/agent/credential_pool.py index 4286903c0e..8d23c66ac5 100644 --- a/agent/credential_pool.py +++ b/agent/credential_pool.py @@ -450,7 +450,7 @@ def _iter_custom_providers(config: Optional[dict] = None): if config is None: return try: - from hermes_cli.config import get_compatible_custom_providers + from hermes_cli.config_providers import get_compatible_custom_providers custom_providers = get_compatible_custom_providers(config) except Exception: diff --git a/agent/model_metadata.py b/agent/model_metadata.py index 1bbf221ddf..ed98dc9167 100644 --- a/agent/model_metadata.py +++ b/agent/model_metadata.py @@ -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 import get_custom_provider_tls_settings + from hermes_cli.config_providers import get_custom_provider_tls_settings tls = get_custom_provider_tls_settings(base_url) if tls.get("ssl_verify") is False: return False @@ -1739,7 +1739,8 @@ 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 get_compatible_custom_providers, load_config + from hermes_cli.config import load_config + from hermes_cli.config_providers import get_compatible_custom_providers from hermes_cli.moa_config import resolve_moa_preset from hermes_cli.runtime_provider import resolve_runtime_provider config = load_config() @@ -1776,7 +1777,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 import get_custom_provider_context_length + from hermes_cli.config_providers 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 diff --git a/agent/system_prompt.py b/agent/system_prompt.py index 3c47a66340..a7978d3d7a 100644 --- a/agent/system_prompt.py +++ b/agent/system_prompt.py @@ -138,10 +138,8 @@ 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, PLUGIN_SECTIONS_END, PLUGIN_SECTIONS_START, - RenderedPluginSystemPromptSection, format_system_prompt_sections, - ) + 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 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: @@ -167,7 +165,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 import format_system_prompt_sections + from hermes_cli.plugins_dispatch import format_system_prompt_sections block = format_system_prompt_sections([s for s in sections if s.position == position]) return [block] if block else [] diff --git a/cli.py b/cli.py index 6ee26558a6..7b9b6baf2e 100644 --- a/cli.py +++ b/cli.py @@ -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 import _get_named_custom_provider + from hermes_cli.runtime_provider_custom import _get_named_custom_provider _named_custom = _get_named_custom_provider(provider) except Exception as exc: diff --git a/gateway/relay/ws_transport.py b/gateway/relay/ws_transport.py index 3dfbedae22..fbf35539d7 100644 --- a/gateway/relay/ws_transport.py +++ b/gateway/relay/ws_transport.py @@ -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 import slack_subcommand_map + from hermes_cli.commands_platforms import slack_subcommand_map payload = parent_parts[1].strip() if len(parent_parts) > 1 else "" subcommand_map = slack_subcommand_map() diff --git a/gateway/run.py b/gateway/run.py index 8f93ea28de..dcc7829401 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -829,7 +829,7 @@ def _telegramize_command_mentions(text: str, platform: Any) -> str: if platform_value != "telegram": return text - from hermes_cli.commands import _sanitize_telegram_name + from hermes_cli.commands_platforms 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 import get_compatible_custom_providers + from hermes_cli.config_providers 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 import get_custom_provider_context_length + from hermes_cli.config_providers import get_custom_provider_context_length return get_custom_provider_context_length( model=resolved_model, base_url=base_url, custom_providers=custom_providers) diff --git a/gateway/run_config_loaders.py b/gateway/run_config_loaders.py index 4bc0e1ed6d..008d6d88fd 100644 --- a/gateway/run_config_loaders.py +++ b/gateway/run_config_loaders.py @@ -25,7 +25,8 @@ 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, resolve_ephemeral_system_prompt_from_config +from hermes_cli.config import cfg_get +from hermes_cli.personality import resolve_ephemeral_system_prompt from hermes_cli.fallback_config import get_fallback_chain from utils import is_truthy_value @@ -93,7 +94,7 @@ class GatewayConfigLoadersMixin: prompt = os.getenv("HERMES_EPHEMERAL_SYSTEM_PROMPT", "") if prompt: return prompt - return resolve_ephemeral_system_prompt_from_config(_load_gateway_runtime_config()) + return resolve_ephemeral_system_prompt(_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).""" diff --git a/gateway/run_inbound.py b/gateway/run_inbound.py index 989ccf794e..34811dbcf5 100644 --- a/gateway/run_inbound.py +++ b/gateway/run_inbound.py @@ -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 import get_compatible_custom_providers + from hermes_cli.config_providers 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 import get_custom_provider_context_length + from hermes_cli.config_providers 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, diff --git a/gateway/run_turn.py b/gateway/run_turn.py index 3b7ee873fc..a98ffbb894 100644 --- a/gateway/run_turn.py +++ b/gateway/run_turn.py @@ -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 import ( + from hermes_cli.config_providers 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: diff --git a/gateway/slash_commands_model.py b/gateway/slash_commands_model.py index 3024f6380d..0451fbf644 100644 --- a/gateway/slash_commands_model.py +++ b/gateway/slash_commands_model.py @@ -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 import get_compatible_custom_providers + from hermes_cli.config_providers import get_compatible_custom_providers self.custom_provs = get_compatible_custom_providers(cfg) except Exception: self.custom_provs = cfg.get("custom_providers") diff --git a/hermes_cli/auth_commands.py b/hermes_cli/auth_commands.py index a3c76ad564..d0a9249552 100644 --- a/hermes_cli/auth_commands.py +++ b/hermes_cli/auth_commands.py @@ -30,7 +30,8 @@ _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 get_compatible_custom_providers, load_config + from hermes_cli.config import load_config + from hermes_cli.config_providers import get_compatible_custom_providers config = load_config() except Exception: return [] diff --git a/hermes_cli/cli_model_switch_mixin.py b/hermes_cli/cli_model_switch_mixin.py index 5176c75c15..07df4c9c77 100644 --- a/hermes_cli/cli_model_switch_mixin.py +++ b/hermes_cli/cli_model_switch_mixin.py @@ -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 import canonical_custom_identity + from hermes_cli.runtime_provider_custom import canonical_custom_identity return canonical_custom_identity(base_url=base_url or None, model=model or None) or None except Exception: return None diff --git a/hermes_cli/cli_tui_mixin.py b/hermes_cli/cli_tui_mixin.py index 07338043b2..10512f74e7 100644 --- a/hermes_cli/cli_tui_mixin.py +++ b/hermes_cli/cli_tui_mixin.py @@ -15,7 +15,7 @@ import threading import time from agent.interrupt_compat import request_hard_interrupt -from hermes_cli.commands import SlashCommandAutoSuggest, SlashCommandCompleter +from hermes_cli.commands_completion import SlashCommandAutoSuggest, SlashCommandCompleter from pathlib import Path from prompt_toolkit.filters import Condition from prompt_toolkit.history import FileHistory diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index 13173f05f1..7441606d0f 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -467,26 +467,3 @@ 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) diff --git a/hermes_cli/commands_completion.py b/hermes_cli/commands_completion.py index 30e5d33624..b4ca20fccb 100644 --- a/hermes_cli/commands_completion.py +++ b/hermes_cli/commands_completion.py @@ -1,5 +1,5 @@ """prompt_toolkit completer + inline auto-suggest for slash commands. Kept out of -:mod:`hermes_cli.commands` (which re-exports both classes) so the registry module stays +:mod:`hermes_cli.commands` so the registry module stays prompt_toolkit-free for the gateway.""" from __future__ import annotations diff --git a/hermes_cli/commands_platforms.py b/hermes_cli/commands_platforms.py index cbccf01a68..db664155c6 100644 --- a/hermes_cli/commands_platforms.py +++ b/hermes_cli/commands_platforms.py @@ -148,9 +148,7 @@ 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.""" - # 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() + menu_cfg = _telegram_command_menu_config() configured_rank = _sanitized_rank(menu_cfg["priority"]) default_rank = _sanitized_rank(_TELEGRAM_MENU_PRIORITY) tiers = _TELEGRAM_PRIORITY_TIERS[menu_cfg["priority_mode"]] @@ -272,9 +270,7 @@ 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.""" - # 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)) + core_commands = list(telegram_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) diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 78848329ac..9f82175fcb 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -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, get_process_hermes_home # noqa: F401 +from hermes_constants import get_hermes_home from utils import atomic_replace, atomic_yaml_write, fast_safe_load logger = logging.getLogger(__name__) @@ -686,23 +686,7 @@ 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.) - _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 ---- diff --git a/hermes_cli/config_migrations.py b/hermes_cli/config_migrations.py index 46c27177bb..a3c3a706a9 100644 --- a/hermes_cli/config_migrations.py +++ b/hermes_cli/config_migrations.py @@ -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. - _custom_provider_entry_to_provider_config = _cfg()._custom_provider_entry_to_provider_config + from hermes_cli.config_providers import _custom_provider_entry_to_provider_config config = read_raw_config() custom_list = config.get("custom_providers") diff --git a/hermes_cli/config_providers.py b/hermes_cli/config_providers.py index 3425c60fb2..1c7548ce9b 100644 --- a/hermes_cli/config_providers.py +++ b/hermes_cli/config_providers.py @@ -357,7 +357,6 @@ 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) @@ -487,7 +486,6 @@ 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: @@ -520,7 +518,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 get_compatible_custom_providers, load_config_readonly + from hermes_cli.config import load_config_readonly if not model or not base_url or not capability: return None if custom_providers is None: diff --git a/hermes_cli/doctor_config.py b/hermes_cli/doctor_config.py index 318132dbdc..ca5285be7c 100644 --- a/hermes_cli/doctor_config.py +++ b/hermes_cli/doctor_config.py @@ -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 import get_compatible_custom_providers + from hermes_cli.config_providers 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 import is_provider_enabled + from hermes_cli.config_providers 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 (): diff --git a/hermes_cli/inventory.py b/hermes_cli/inventory.py index 5cd90fb988..df916e9c5c 100644 --- a/hermes_cli/inventory.py +++ b/hermes_cli/inventory.py @@ -38,8 +38,9 @@ class ConfigContext: def load_picker_context() -> ConfigContext: """Load the disk-config snapshot every consumer needs.""" - from hermes_cli.config import ( - coerce_provider_id, get_compatible_custom_providers, load_config, stringify_provider_map, + from hermes_cli.config import load_config + from hermes_cli.config_providers import ( + coerce_provider_id, get_compatible_custom_providers, stringify_provider_map ) cfg = load_config() model_cfg = cfg.get("model", {}) diff --git a/hermes_cli/kanban.py b/hermes_cli/kanban.py index d1734d8a65..c057091473 100644 --- a/hermes_cli/kanban.py +++ b/hermes_cli/kanban.py @@ -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, re-exported ``build_parser``), +DB work lives in ``kanban_db``; siblings: ``kanban_parser`` (argparse ``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 # noqa: F401 (re-exported: hermes_cli.main, run_slash) +from hermes_cli.kanban_parser import build_parser # --- Flag parsing helpers --- diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 99171b98e8..0566b45a14 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -1999,7 +1999,8 @@ 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_compatible_custom_providers, get_env_value + from hermes_cli.config import get_env_value + from hermes_cli.config_providers import get_compatible_custom_providers from hermes_cli.providers import custom_provider_aliases, resolve_provider_full active = "" diff --git a/hermes_cli/main_provider_setup.py b/hermes_cli/main_provider_setup.py index a8a89fb974..5ffd586c46 100644 --- a/hermes_cli/main_provider_setup.py +++ b/hermes_cli/main_provider_setup.py @@ -709,7 +709,8 @@ 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 get_compatible_custom_providers, read_raw_config + from hermes_cli.config import read_raw_config + from hermes_cli.config_providers import get_compatible_custom_providers from hermes_cli.providers import custom_provider_slug # Raw (un-expanded) templates keyed by identity. ``get_compatible_custom_providers( diff --git a/hermes_cli/model_setup_flows_custom.py b/hermes_cli/model_setup_flows_custom.py index 91d101b906..9d2272308f 100644 --- a/hermes_cli/model_setup_flows_custom.py +++ b/hermes_cli/model_setup_flows_custom.py @@ -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 import normalize_extra_headers + from hermes_cli.config_providers 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, diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index 6649653f60..64d4fcd501 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -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 import is_provider_enabled + from hermes_cli.config_providers 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 import _get_named_custom_provider, _custom_provider_request_overrides + from hermes_cli.runtime_provider_custom 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 import normalize_extra_headers + from hermes_cli.config_providers import normalize_extra_headers return normalize_extra_headers(entry.get("extra_headers")) diff --git a/hermes_cli/model_switch_providers.py b/hermes_cli/model_switch_providers.py index 548460e063..e205876eb3 100644 --- a/hermes_cli/model_switch_providers.py +++ b/hermes_cli/model_switch_providers.py @@ -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 import coerce_provider_id, is_provider_enabled + from hermes_cli.config_providers 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 import coerce_provider_id + from hermes_cli.config_providers 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 import coerce_provider_id, stringify_provider_map + from hermes_cli.config_providers 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 import is_provider_enabled + from hermes_cli.config_providers import is_provider_enabled if isinstance(user_providers, dict): disabled = { str(name).strip().lower() for name, cfg in user_providers.items() diff --git a/hermes_cli/models.py b/hermes_cli/models.py index 541d23f1e8..620744ba7a 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -124,7 +124,7 @@ def _custom_provider_ssl_context(base_url: str): if not base_url: return None try: - from hermes_cli.config import get_custom_provider_tls_settings + from hermes_cli.config_providers 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 import normalize_extra_headers + from hermes_cli.config_providers import normalize_extra_headers headers.update(normalize_extra_headers(request_headers)) diff --git a/hermes_cli/models_local.py b/hermes_cli/models_local.py index 2ffc968e20..193ced3a02 100644 --- a/hermes_cli/models_local.py +++ b/hermes_cli/models_local.py @@ -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 import normalize_extra_headers + from hermes_cli.config_providers import normalize_extra_headers result = normalize_extra_headers(entry.get("extra_headers")) except (ImportError, OSError, RuntimeError, TypeError, ValueError): diff --git a/hermes_cli/plugin_dev.py b/hermes_cli/plugin_dev.py index 3718c7b631..dcf7963c0d 100644 --- a/hermes_cli/plugin_dev.py +++ b/hermes_cli/plugin_dev.py @@ -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 import SUPPORTED_MANIFEST_VERSION + from hermes_cli.plugins_manifest 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 import _CONFIG_SCHEMA_TYPES + from hermes_cli.plugins_manifest 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: diff --git a/hermes_cli/plugins.py b/hermes_cli/plugins.py index 1cf03f9984..ac4f4a8128 100644 --- a/hermes_cli/plugins.py +++ b/hermes_cli/plugins.py @@ -35,27 +35,21 @@ 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 ( # 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_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_loader import ( PluginLoaderMixin, _BARE_MODULE_SCOPE, _MODULE_NAMESPACE_LOCK, _NS_PARENT, _evict_modules, _plugin_home_scope, _serialized_replacement, ) -from hermes_cli.plugins_dispatch import ( # noqa: F401 — re-exported +from hermes_cli.plugins_dispatch import ( DEFAULT_SYSTEM_PROMPT_SECTION_MAX_CHARS, HERMES_EVENT_NAMESPACE, MAX_SYSTEM_PROMPT_SECTION_CHARS, - 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, + 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 ) from hermes_cli.plugins_ledger import PluginLedgerMixin, PluginRegistration from hermes_cli.plugins_state import ( @@ -1406,10 +1400,6 @@ 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 [ @@ -1798,23 +1788,6 @@ 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]: diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index d1807bdc2c..d9c4cd0e80 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -12,22 +12,19 @@ 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 ( # 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.credential_pool import CredentialPool, PooledCredential, credential_pool_matches_provider, load_pool from agent.secret_scope import get_secret as _get_secret -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.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 import config as _config_mod from hermes_cli import models as _models # attribute access keeps ``hermes_cli.models.`` patches effective @@ -44,11 +41,11 @@ def load_config(): def get_compatible_custom_providers(config=None): - return _config_mod.get_compatible_custom_providers(config) + return config_providers.get_compatible_custom_providers(config) def normalize_extra_headers(value): - return _config_mod.normalize_extra_headers(value) + return config_providers.normalize_extra_headers(value) def _getenv(name: str, default: str = "") -> str: @@ -135,7 +132,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_mod._canonical_api_mode(raw).lower() if isinstance(raw, str) else "" + normalized = config_providers._canonical_api_mode(raw).lower() if isinstance(raw, str) else "" return normalized if normalized in _VALID_API_MODES else None @@ -402,13 +399,7 @@ def resolve_requested_provider(requested: Optional[str] = None) -> str: # ── extracted collaborators (re-exported; see module docstring) ──────────────────────────── -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_custom import _resolve_named_custom_runtime # noqa: E402 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, @@ -733,7 +724,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_mod.is_provider_enabled(block): + if isinstance(block, dict) and not config_providers.is_provider_enabled(block): raise ValueError(f"provider {requested_provider!r} is disabled in config " f"(providers.{requested_provider}.enabled: false)") diff --git a/hermes_cli/runtime_provider_backends.py b/hermes_cli/runtime_provider_backends.py index 2ce619a3ab..f12a276e8a 100644 --- a/hermes_cli/runtime_provider_backends.py +++ b/hermes_cli/runtime_provider_backends.py @@ -157,7 +157,8 @@ 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: - pool_result = rp._try_resolve_from_custom_pool(base_url, "custom", cfg_api_mode, provider_name=None) + 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) if pool_result: return pool_result # Local no-auth servers get a placeholder key — the OpenAI SDK requires a non-empty string. @@ -248,7 +249,8 @@ def _is_external_process_provider(provider: str) -> bool: def _resolve_external_process_runtime(provider: str, requested_provider: str) -> Dict[str, Any]: rp = _rp() - creds = rp.resolve_external_process_provider_credentials(provider) + from hermes_cli.auth import resolve_external_process_provider_credentials + creds = 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) diff --git a/hermes_cli/runtime_provider_custom.py b/hermes_cli/runtime_provider_custom.py index 8981ab2368..2d32fc0aad 100644 --- a/hermes_cli/runtime_provider_custom.py +++ b/hermes_cli/runtime_provider_custom.py @@ -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 import is_provider_enabled + from hermes_cli.config_providers import is_provider_enabled rp = _rp() for ep_name, entry in providers.items(): # ``providers..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 _rp()._get_named_custom_provider(requested_provider) is not None + return _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 = rp._get_named_custom_provider(candidate) + entry = _get_named_custom_provider(candidate) except Exception: return None if entry is None: @@ -331,8 +331,9 @@ 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(rp.custom_provider_pool_key_candidates(base_url, provider_name)) + raw_keys = list(custom_provider_pool_key_candidates(base_url, provider_name)) except Exception: raw_keys = [] # Order-preserving dedupe of normalized keys. @@ -426,7 +427,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 = rp._try_resolve_from_custom_pool(base_url, "custom", None) + pool_result = _try_resolve_from_custom_pool(base_url, "custom", None) if pool_result: pool_result["source"] = "direct-alias" return pool_result @@ -475,13 +476,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 = rp._get_named_custom_provider(requested_provider) + custom_provider = _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 = rp._try_resolve_from_custom_pool( + pool_result = _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"), ) diff --git a/hermes_cli/secrets_cli.py b/hermes_cli/secrets_cli.py index 2398b1e5dd..1e38f7acaa 100644 --- a/hermes_cli/secrets_cli.py +++ b/hermes_cli/secrets_cli.py @@ -52,20 +52,6 @@ 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 ── diff --git a/hermes_cli/slack_cli.py b/hermes_cli/slack_cli.py index 354133d2a3..1c89955d8e 100644 --- a/hermes_cli/slack_cli.py +++ b/hermes_cli/slack_cli.py @@ -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 import slack_app_manifest + from hermes_cli.commands_platforms 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 import slack_app_manifest + from hermes_cli.commands_platforms import slack_app_manifest manifest = slack_app_manifest()["features"]["slash_commands"] else: manifest = _build_full_manifest( diff --git a/hermes_cli/web_routers/config_env.py b/hermes_cli/web_routers/config_env.py index 1592cc63a3..4a5d12b766 100644 --- a/hermes_cli/web_routers/config_env.py +++ b/hermes_cli/web_routers/config_env.py @@ -14,7 +14,10 @@ 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, coerce_provider_id, find_provider_entry, redact_key, _deep_merge +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.web_models import ConfigUpdate, EnvVarUpdate, EnvVarDelete, EnvVarReveal, CustomEndpointUpdate from typing import Any, Dict, List, Optional, Tuple diff --git a/hermes_cli/web_server_config.py b/hermes_cli/web_server_config.py index 6da1a7536e..2f1365593a 100644 --- a/hermes_cli/web_server_config.py +++ b/hermes_cli/web_server_config.py @@ -10,14 +10,10 @@ 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, - find_provider_entry, - read_raw_config, - resolve_cron_model_drift_defaults, + DEFAULT_CONFIG, build_cron_model_impact, cfg_get, clear_model_endpoint_credentials, 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). @@ -394,7 +390,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 import get_compatible_custom_providers + from hermes_cli.config_providers 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 diff --git a/hermes_cli/web_server_dashboard.py b/hermes_cli/web_server_dashboard.py index 5898ec638b..41599b50d0 100644 --- a/hermes_cli/web_server_dashboard.py +++ b/hermes_cli/web_server_dashboard.py @@ -18,7 +18,8 @@ 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, get_process_hermes_home +from hermes_cli.config import cfg_get +from hermes_constants import get_process_hermes_home from utils import env_var_enabled # Same logger the code used before extraction (record parity). diff --git a/hermes_cli/web_server_profiles.py b/hermes_cli/web_server_profiles.py index fcc64f90a4..b5be1d889f 100644 --- a/hermes_cli/web_server_profiles.py +++ b/hermes_cli/web_server_profiles.py @@ -16,7 +16,8 @@ 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, get_process_hermes_home +from hermes_cli.config import DEFAULT_CONFIG +from hermes_constants import 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 diff --git a/plugins/memory/__init__.py b/plugins/memory/__init__.py index f7d2eff298..88fd2f9765 100644 --- a/plugins/memory/__init__.py +++ b/plugins/memory/__init__.py @@ -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 import resolve_module_origin + from hermes_cli.plugins_manifest import resolve_module_origin module_name = (entry_point.value or "").split(":")[0].strip() origin = resolve_module_origin(module_name) diff --git a/plugins/platforms/discord/adapter.py b/plugins/platforms/discord/adapter.py index 6101704e57..09422b9693 100644 --- a/plugins/platforms/discord/adapter.py +++ b/plugins/platforms/discord/adapter.py @@ -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 import discord_skill_commands_by_category + from hermes_cli.commands_platforms 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), diff --git a/plugins/platforms/slack/adapter.py b/plugins/platforms/slack/adapter.py index 0ad49182de..6afa048c8c 100644 --- a/plugins/platforms/slack/adapter.py +++ b/plugins/platforms/slack/adapter.py @@ -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 import slack_native_slashes + from hermes_cli.commands_platforms 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 import slack_subcommand_map + from hermes_cli.commands_platforms import slack_subcommand_map subcommand_map = slack_subcommand_map() subcommand_map["compact"] = "/compress" first_word = legacy_text.split()[0] if legacy_text.split() else "" diff --git a/plugins/platforms/telegram/adapter.py b/plugins/platforms/telegram/adapter.py index 9d4737371e..36da8a65fb 100644 --- a/plugins/platforms/telegram/adapter.py +++ b/plugins/platforms/telegram/adapter.py @@ -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 import telegram_menu_commands, telegram_menu_max_commands + from hermes_cli.commands_platforms 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 import telegram_menu_commands, telegram_menu_max_commands + from hermes_cli.commands_platforms 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)) diff --git a/plugins/platforms/telegram/inline_picker.py b/plugins/platforms/telegram/inline_picker.py index fa3267ac54..59eb5bce58 100644 --- a/plugins/platforms/telegram/inline_picker.py +++ b/plugins/platforms/telegram/inline_picker.py @@ -33,7 +33,9 @@ def collect_inline_catalog() -> List[Dict[str, str]]: catalog.append({"name": name, "description": desc or ""}) try: - from hermes_cli.commands import _collect_gateway_skill_entries, _sanitize_telegram_name, telegram_bot_commands + from hermes_cli.commands_platforms 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 diff --git a/tests/agent/test_auxiliary_client_ssl_verify.py b/tests/agent/test_auxiliary_client_ssl_verify.py index 1439f33c8a..892dd67bf3 100644 --- a/tests/agent/test_auxiliary_client_ssl_verify.py +++ b/tests/agent/test_auxiliary_client_ssl_verify.py @@ -15,6 +15,7 @@ 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") @@ -43,7 +44,7 @@ def test_resolve_aux_verify_ssl_verify_false(clean_tls_env, monkeypatch): from agent import auxiliary_client monkeypatch.setattr( - cfg, + config_providers, "get_custom_provider_tls_settings", lambda *a, **k: {"ssl_verify": False}, ) diff --git a/tests/agent/test_auxiliary_named_custom_providers.py b/tests/agent/test_auxiliary_named_custom_providers.py index 40336b720c..f46aede7dd 100644 --- a/tests/agent/test_auxiliary_named_custom_providers.py +++ b/tests/agent/test_auxiliary_named_custom_providers.py @@ -264,7 +264,7 @@ class TestProvidersDictApiModeAnthropicMessages: }, }, }) - from hermes_cli.runtime_provider import _get_named_custom_provider + from hermes_cli.runtime_provider_custom import _get_named_custom_provider entry = _get_named_custom_provider("myrelay") assert entry is not None assert entry.get("api_mode") == "anthropic_messages" diff --git a/tests/agent/test_command_token_source.py b/tests/agent/test_command_token_source.py index e7210480ef..234d6f5e76 100644 --- a/tests/agent/test_command_token_source.py +++ b/tests/agent/test_command_token_source.py @@ -25,6 +25,7 @@ from agent.command_token_source import ( _mint, build_command_token_provider, ) +from hermes_cli import runtime_provider_custom class TestMinting: @@ -311,7 +312,7 @@ class TestAuxiliaryResolverHonoursKeyCmd: from hermes_cli import runtime_provider as rp monkeypatch.setattr( - rp, "_get_named_custom_provider", + runtime_provider_custom, "_get_named_custom_provider", lambda name: dict(entry, name="dbx") if name == "dbx" else None, ) seen = {} diff --git a/tests/agent/test_custom_provider_ca_probes.py b/tests/agent/test_custom_provider_ca_probes.py index 1c32edc3fc..79a72d9e9c 100644 --- a/tests/agent/test_custom_provider_ca_probes.py +++ b/tests/agent/test_custom_provider_ca_probes.py @@ -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.get_compatible_custom_providers", + "hermes_cli.config_providers.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.get_compatible_custom_providers", + "hermes_cli.config_providers.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.get_compatible_custom_providers", + "hermes_cli.config_providers.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.get_compatible_custom_providers", probe): + with patch("hermes_cli.config_providers.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.get_compatible_custom_providers", + "hermes_cli.config_providers.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.get_compatible_custom_providers", + "hermes_cli.config_providers.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.get_compatible_custom_providers", + "hermes_cli.config_providers.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.get_compatible_custom_providers", + "hermes_cli.config_providers.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.get_compatible_custom_providers", + "hermes_cli.config_providers.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.get_compatible_custom_providers", + "hermes_cli.config_providers.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.get_compatible_custom_providers", + "hermes_cli.config_providers.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.get_compatible_custom_providers", + "hermes_cli.config_providers.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.get_compatible_custom_providers", + "hermes_cli.config_providers.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.get_compatible_custom_providers", + "hermes_cli.config_providers.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.get_compatible_custom_providers", + "hermes_cli.config_providers.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.get_compatible_custom_providers", + "hermes_cli.config_providers.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.get_compatible_custom_providers", + "hermes_cli.config_providers.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.get_compatible_custom_providers", + "hermes_cli.config_providers.get_compatible_custom_providers", return_value=[], ), patch.object( models, "_urlopen_model_catalog_request", side_effect=_strict_two_arg diff --git a/tests/agent/test_plan_prompt.py b/tests/agent/test_plan_prompt.py index 63cf357ce9..87abbe0f26 100644 --- a/tests/agent/test_plan_prompt.py +++ b/tests/agent/test_plan_prompt.py @@ -68,7 +68,7 @@ class TestPlanRegistryWiring: assert "plan" in GATEWAY_KNOWN_COMMANDS def test_plan_in_telegram_bot_commands(self): - from hermes_cli.commands import telegram_bot_commands + from hermes_cli.commands_platforms import telegram_bot_commands names = {n for n, _ in telegram_bot_commands()} assert "plan" in names diff --git a/tests/cli/test_cli_provider_resolution.py b/tests/cli/test_cli_provider_resolution.py index 46336f95cc..17d4df12e9 100644 --- a/tests/cli/test_cli_provider_resolution.py +++ b/tests/cli/test_cli_provider_resolution.py @@ -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._get_named_custom_provider", + "hermes_cli.runtime_provider_custom._get_named_custom_provider", _boom, ) diff --git a/tests/cli/test_resume_model_restore.py b/tests/cli/test_resume_model_restore.py index aa01462527..8ecbc7892e 100644 --- a/tests/cli/test_resume_model_restore.py +++ b/tests/cli/test_resume_model_restore.py @@ -12,6 +12,7 @@ import pytest import cli as cli_mod from hermes_state import SessionDB +from hermes_cli import runtime_provider_custom def _make_stub(**overrides): @@ -231,7 +232,7 @@ def test_persist_model_switch_heals_bare_custom(monkeypatch): api_mode = "" import hermes_cli.runtime_provider as rp - monkeypatch.setattr(rp, "canonical_custom_identity", + monkeypatch.setattr(runtime_provider_custom, "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()) @@ -239,7 +240,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(rp, "canonical_custom_identity", + monkeypatch.setattr(runtime_provider_custom, "canonical_custom_identity", lambda base_url=None, model=None: None) written.clear() stub._persist_model_switch_to_session(_BareResult()) @@ -250,7 +251,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(rp, "canonical_custom_identity", + monkeypatch.setattr(runtime_provider_custom, "canonical_custom_identity", lambda base_url=None, model=None: None) stub = _make_stub() stub._restore_session_model(_row(model_config={ diff --git a/tests/gateway/test_background_command.py b/tests/gateway/test_background_command.py index c137542285..c240abcdfc 100644 --- a/tests/gateway/test_background_command.py +++ b/tests/gateway/test_background_command.py @@ -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 import SlashCommandCompleter + from hermes_cli.commands_completion import SlashCommandCompleter from prompt_toolkit.document import Document completer = SlashCommandCompleter() diff --git a/tests/gateway/test_discord_slash_commands.py b/tests/gateway/test_discord_slash_commands.py index 4f602a8768..e636901a70 100644 --- a/tests/gateway/test_discord_slash_commands.py +++ b/tests/gateway/test_discord_slash_commands.py @@ -530,7 +530,7 @@ def test_register_skill_command_callback_dispatches_by_name(adapter): ] with patch( - "hermes_cli.commands.discord_skill_commands_by_category", + "hermes_cli.commands_platforms.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.discord_skill_commands_by_category", + "hermes_cli.commands_platforms.discord_skill_commands_by_category", return_value=(large_categories, [], 0), ): adapter._register_slash_commands() diff --git a/tests/gateway/test_reload_skills_discord_resync.py b/tests/gateway/test_reload_skills_discord_resync.py index fa1b6bcf3f..e14562ce6b 100644 --- a/tests/gateway/test_reload_skills_discord_resync.py +++ b/tests/gateway/test_reload_skills_discord_resync.py @@ -69,7 +69,7 @@ class TestRefreshSkillGroup: ) monkeypatch.setattr( - "hermes_cli.commands.discord_skill_commands_by_category", + "hermes_cli.commands_platforms.discord_skill_commands_by_category", fake_collector, ) @@ -113,7 +113,7 @@ class TestRegisterSkillGroupUsesInstanceState: 0, ) monkeypatch.setattr( - "hermes_cli.commands.discord_skill_commands_by_category", + "hermes_cli.commands_platforms.discord_skill_commands_by_category", fake_collector, ) diff --git a/tests/gateway/test_session_info.py b/tests/gateway/test_session_info.py index 7d16d6b807..7e020294f3 100644 --- a/tests/gateway/test_session_info.py +++ b/tests/gateway/test_session_info.py @@ -95,7 +95,7 @@ class TestFormatSessionInfo: }, ) with p1, p2, p3, patch( - "hermes_cli.config.get_compatible_custom_providers", + "hermes_cli.config_providers.get_compatible_custom_providers", return_value=[ { "name": "custom-local-agentw", diff --git a/tests/gateway/test_status_command.py b/tests/gateway/test_status_command.py index ec179816c7..177c237724 100644 --- a/tests/gateway/test_status_command.py +++ b/tests/gateway/test_status_command.py @@ -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.get_compatible_custom_providers", + "hermes_cli.config_providers.get_compatible_custom_providers", return_value=config["custom_providers"], ), patch( "agent.model_metadata.get_model_context_length", diff --git a/tests/gateway/test_telegram_forum_commands.py b/tests/gateway/test_telegram_forum_commands.py index 4ab831e2dd..9cdbe3a109 100644 --- a/tests/gateway/test_telegram_forum_commands.py +++ b/tests/gateway/test_telegram_forum_commands.py @@ -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.telegram_menu_commands") as mock_menu: + with patch("hermes_cli.commands_platforms.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.telegram_menu_commands") as mock_menu: + with patch("hermes_cli.commands_platforms.telegram_menu_commands") as mock_menu: mock_menu.return_value = ([("new", "Start new session")], 0) with patch("telegram.BotCommand"): with patch("telegram.BotCommandScopeChat"): diff --git a/tests/hermes_cli/test_anthropic_oauth_routes_to_messages_api.py b/tests/hermes_cli/test_anthropic_oauth_routes_to_messages_api.py index da08a873f2..1095a5ce2e 100644 --- a/tests/hermes_cli/test_anthropic_oauth_routes_to_messages_api.py +++ b/tests/hermes_cli/test_anthropic_oauth_routes_to_messages_api.py @@ -27,6 +27,8 @@ 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: @@ -146,10 +148,10 @@ class TestCustomProviderUrlFallback: def select(self): return _Entry() - monkeypatch.setattr(rp, "custom_provider_pool_key_candidates", lambda *a, **k: ["custom:my-claude"]) + monkeypatch.setattr(credential_pool, "custom_provider_pool_key_candidates", lambda *a, **k: ["custom:my-claude"]) monkeypatch.setattr(rp, "load_pool", lambda key: _Pool()) - resolved = rp._try_resolve_from_custom_pool( + resolved = runtime_provider_custom._try_resolve_from_custom_pool( "https://api.anthropic.com", "custom", ) diff --git a/tests/hermes_cli/test_api_mode_aliases.py b/tests/hermes_cli/test_api_mode_aliases.py index 550e009a15..9c51527670 100644 --- a/tests/hermes_cli/test_api_mode_aliases.py +++ b/tests/hermes_cli/test_api_mode_aliases.py @@ -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._normalize_custom_provider_entry`` passed the raw +* ``hermes_cli.config_providers._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,8 +28,9 @@ from __future__ import annotations import pytest -from hermes_cli.config import _canonical_api_mode, _normalize_custom_provider_entry +from hermes_cli.config_providers 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: @@ -65,7 +66,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 import _API_MODE_ALIASES + from hermes_cli.config_providers import _API_MODE_ALIASES for target in _API_MODE_ALIASES.values(): assert target in _VALID_API_MODES diff --git a/tests/hermes_cli/test_approvals_command.py b/tests/hermes_cli/test_approvals_command.py index 011ba7de2c..470869b616 100644 --- a/tests/hermes_cli/test_approvals_command.py +++ b/tests/hermes_cli/test_approvals_command.py @@ -6,14 +6,9 @@ from unittest.mock import MagicMock, patch import yaml from cli import HermesCLI -from hermes_cli.commands import ( - GATEWAY_KNOWN_COMMANDS, - SUBCOMMANDS, - SlashCommandCompleter, - gateway_help_lines, - resolve_command, - telegram_bot_commands, -) +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 prompt_toolkit.completion import CompleteEvent from prompt_toolkit.document import Document diff --git a/tests/hermes_cli/test_at_context_completion_filter.py b/tests/hermes_cli/test_at_context_completion_filter.py index d6121753c8..1ba0bfee1e 100644 --- a/tests/hermes_cli/test_at_context_completion_filter.py +++ b/tests/hermes_cli/test_at_context_completion_filter.py @@ -14,7 +14,7 @@ from __future__ import annotations from pathlib import Path from typing import Iterable -from hermes_cli.commands import SlashCommandCompleter +from hermes_cli.commands_completion import SlashCommandCompleter def _run(tmp_path: Path, word: str) -> list[tuple[str, str]]: diff --git a/tests/hermes_cli/test_bitwarden_status.py b/tests/hermes_cli/test_bitwarden_status.py index d9a86305ff..604b64781d 100644 --- a/tests/hermes_cli/test_bitwarden_status.py +++ b/tests/hermes_cli/test_bitwarden_status.py @@ -22,6 +22,7 @@ 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 = {} @@ -32,7 +33,7 @@ def test_status_surfaces_failed_token_validation(monkeypatch, capsys): ) monkeypatch.setenv("BWS_ACCESS_TOKEN", "0.invalid-token") monkeypatch.setattr( - secrets_cli.bw, + bw, "find_bws", lambda install_if_missing=False: Path("/tmp/bws"), ) @@ -68,11 +69,12 @@ 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( - secrets_cli.bw, + bw, "find_bws", lambda install_if_missing=False: Path("/tmp/bws"), ) @@ -93,11 +95,12 @@ 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( - secrets_cli.bw, + bw, "find_bws", lambda install_if_missing=False: None, ) diff --git a/tests/hermes_cli/test_canonical_custom_identity.py b/tests/hermes_cli/test_canonical_custom_identity.py index 46b8f07c6a..2e6015019c 100644 --- a/tests/hermes_cli/test_canonical_custom_identity.py +++ b/tests/hermes_cli/test_canonical_custom_identity.py @@ -18,6 +18,7 @@ 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" @@ -49,18 +50,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 rp.canonical_custom_identity(config_provider=DISPLAY_NAME) == CANONICAL + assert runtime_provider_custom.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 rp.canonical_custom_identity() == CANONICAL + assert runtime_provider_custom.canonical_custom_identity() == CANONICAL def test_config_key_spelling_still_resolves(keyed_provider_config): """The spelling that already worked keeps working.""" - assert rp.canonical_custom_identity(config_provider=PROVIDER_KEY) == CANONICAL + assert runtime_provider_custom.canonical_custom_identity(config_provider=PROVIDER_KEY) == CANONICAL def test_all_recovery_sources_agree_on_one_identity(keyed_provider_config): @@ -69,16 +70,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 = 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) + 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) 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 rp.canonical_custom_identity(config_provider="not-a-configured-entry") is None + assert runtime_provider_custom.canonical_custom_identity(config_provider="not-a-configured-entry") is None def test_legacy_unkeyed_entry_keeps_its_name_identity(monkeypatch): @@ -97,7 +98,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 rp.canonical_custom_identity(config_provider="Legacy Endpoint") == "custom:legacy-endpoint" + assert runtime_provider_custom.canonical_custom_identity(config_provider="Legacy Endpoint") == "custom:legacy-endpoint" class TestIsRoutableProvider: @@ -108,25 +109,25 @@ class TestIsRoutableProvider: """ def test_empty_auto_and_builtin_are_routable(self, keyed_provider_config): - 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 + 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 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 rp.is_routable_provider("custom") is False + assert runtime_provider_custom.is_routable_provider("custom") is False def test_registered_names_are_routable(self, keyed_provider_config): - assert rp.is_routable_provider(PROVIDER_KEY) is True - assert rp.is_routable_provider(CANONICAL) is True + assert runtime_provider_custom.is_routable_provider(PROVIDER_KEY) is True + assert runtime_provider_custom.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 rp.is_routable_provider("stale-endpoint") is False - assert rp.is_routable_provider("custom:stale-endpoint") is False + assert runtime_provider_custom.is_routable_provider("stale-endpoint") is False + assert runtime_provider_custom.is_routable_provider("custom:stale-endpoint") is False def test_legacy_unkeyed_name_is_routable(self, monkeypatch): config = { @@ -143,6 +144,6 @@ class TestIsRoutableProvider: monkeypatch.setattr("hermes_cli.config.load_config", lambda *a, **k: config) monkeypatch.setattr(rp, "_get_model_config", lambda: {}) - 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 + 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 diff --git a/tests/hermes_cli/test_commands.py b/tests/hermes_cli/test_commands.py index 3f290e05d3..fde57fc924 100644 --- a/tests/hermes_cli/test_commands.py +++ b/tests/hermes_cli/test_commands.py @@ -4,28 +4,13 @@ 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, - 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, + 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 ) @@ -589,7 +574,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 import _collect_gateway_skill_entries + from hermes_cli.commands_platforms import _collect_gateway_skill_entries long_name = "this-is-a-very-long-skill-name-that-exceeds-limit" skills_dir = tmp_path / "skills" @@ -617,7 +602,7 @@ class TestGatewaySkillCollector: def test_cap_trims_skills_only(self, tmp_path): from unittest.mock import patch - from hermes_cli.commands import _collect_gateway_skill_entries + from hermes_cli.commands_platforms import _collect_gateway_skill_entries skills_dir = tmp_path / "skills" skills_dir.mkdir() @@ -814,11 +799,11 @@ class TestTelegramMenuCommands: menu_cfg = {"max_commands": 2, "priority_mode": "prepend", "priority": ["gym"]} with ( - patch("hermes_cli.commands.telegram_bot_commands", return_value=fake_core), + patch("hermes_cli.commands_platforms.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._telegram_command_menu_config", return_value=menu_cfg), + patch("hermes_cli.commands_platforms._telegram_command_menu_config", return_value=menu_cfg), ): menu, hidden = telegram_menu_commands(max_commands=len(fake_core)) @@ -855,10 +840,10 @@ class TestTelegramMenuCommands: menu_cfg = {"max_commands": 2, "priority_mode": "prepend", "priority": []} with ( - patch("hermes_cli.commands.telegram_bot_commands", return_value=fake_core), + patch("hermes_cli.commands_platforms.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._telegram_command_menu_config", return_value=menu_cfg), + patch("hermes_cli.commands_platforms._telegram_command_menu_config", return_value=menu_cfg), ): menu, hidden = telegram_menu_commands(max_commands=2) @@ -891,10 +876,10 @@ class TestTelegramMenuCommands: } with ( - patch("hermes_cli.commands.telegram_bot_commands", return_value=fake_core), + patch("hermes_cli.commands_platforms.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._telegram_command_menu_config", return_value=menu_cfg), + patch("hermes_cli.commands_platforms._telegram_command_menu_config", return_value=menu_cfg), ): menu, hidden = telegram_menu_commands(max_commands=1) @@ -922,7 +907,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._telegram_command_menu_config", return_value=menu_cfg), + patch("hermes_cli.commands_platforms._telegram_command_menu_config", return_value=menu_cfg), ): menu, hidden = telegram_menu_commands(max_commands=1) @@ -957,7 +942,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 import _telegram_command_menu_config + from hermes_cli.commands_platforms import _telegram_command_menu_config raw_config = { "platforms": { @@ -980,7 +965,7 @@ class TestTelegramMenuCommands: # Discord skill commands grouped by category # --------------------------------------------------------------------------- -from hermes_cli.commands import discord_skill_commands_by_category # noqa: E402 +from hermes_cli.commands_platforms import discord_skill_commands_by_category class TestDiscordSkillCommandsByCategory: diff --git a/tests/hermes_cli/test_config.py b/tests/hermes_cli/test_config.py index 3ed68bba14..8419d3e419 100644 --- a/tests/hermes_cli/test_config.py +++ b/tests/hermes_cli/test_config.py @@ -9,29 +9,12 @@ import pytest import yaml from hermes_cli.config import ( - 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, + 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 ) +from hermes_cli.config_providers import get_compatible_custom_providers, is_provider_enabled class TestGetHermesHome: diff --git a/tests/hermes_cli/test_custom_provider_context_length.py b/tests/hermes_cli/test_custom_provider_context_length.py index 2220262a17..b6ea1c5b86 100644 --- a/tests/hermes_cli/test_custom_provider_context_length.py +++ b/tests/hermes_cli/test_custom_provider_context_length.py @@ -8,10 +8,7 @@ from __future__ import annotations from unittest.mock import patch -from hermes_cli.config import ( - get_custom_provider_context_length, - get_custom_provider_model_capability, -) +from hermes_cli.config_providers import get_custom_provider_context_length, get_custom_provider_model_capability class TestGetCustomProviderContextLength: diff --git a/tests/hermes_cli/test_custom_provider_extra_headers.py b/tests/hermes_cli/test_custom_provider_extra_headers.py index c0e668e0e0..35e89775f1 100644 --- a/tests/hermes_cli/test_custom_provider_extra_headers.py +++ b/tests/hermes_cli/test_custom_provider_extra_headers.py @@ -6,11 +6,9 @@ PR #3526 salvage — user-configurable extra HTTP headers on LLM API calls import json -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.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 import models as models_mod diff --git a/tests/hermes_cli/test_custom_provider_identity.py b/tests/hermes_cli/test_custom_provider_identity.py index c4f36ea37f..9b0900a8a7 100644 --- a/tests/hermes_cli/test_custom_provider_identity.py +++ b/tests/hermes_cli/test_custom_provider_identity.py @@ -9,6 +9,7 @@ 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): @@ -22,7 +23,7 @@ def test_matches_legacy_custom_providers_list(monkeypatch): }, ) assert ( - rp.find_custom_provider_identity("https://api.mimo.example/v1") + runtime_provider_custom.find_custom_provider_identity("https://api.mimo.example/v1") == "custom:mimo-v2.5-pro" ) @@ -34,7 +35,7 @@ def test_matches_providers_dict_by_key(monkeypatch): lambda: {"providers": {"local": {"api": "http://127.0.0.1:8000/v1"}}}, ) assert ( - rp.find_custom_provider_identity("http://127.0.0.1:8000/v1") + runtime_provider_custom.find_custom_provider_identity("http://127.0.0.1:8000/v1") == "custom:local" ) @@ -53,10 +54,10 @@ def test_matches_providers_dict_by_stable_key_not_display_name(monkeypatch): "load_config", lambda: config, ) - slug = rp.find_custom_provider_identity("http://127.0.0.1:8000/v1") + slug = runtime_provider_custom.find_custom_provider_identity("http://127.0.0.1:8000/v1") assert slug == "custom:local-127.0.0.1:8000" - entry = rp._get_named_custom_provider(slug) + entry = runtime_provider_custom._get_named_custom_provider(slug) assert entry is not None assert entry["name"] == "Local Ollama" @@ -72,7 +73,7 @@ def test_match_ignores_trailing_slash_and_case(monkeypatch): }, ) assert ( - rp.find_custom_provider_identity("http://localhost:8000/v1") + runtime_provider_custom.find_custom_provider_identity("http://localhost:8000/v1") == "custom:local" ) @@ -87,15 +88,15 @@ def test_no_match_returns_none(monkeypatch): ] }, ) - assert rp.find_custom_provider_identity("https://api.mimo.example/v1") is None + assert runtime_provider_custom.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 rp.find_custom_provider_identity("") is None - assert rp.find_custom_provider_identity(None) is None + assert runtime_provider_custom.find_custom_provider_identity("") is None + assert runtime_provider_custom.find_custom_provider_identity(None) is None def test_identity_resolves_back_through_named_lookup(monkeypatch): @@ -112,10 +113,10 @@ def test_identity_resolves_back_through_named_lookup(monkeypatch): } monkeypatch.setattr(rp, "load_config", lambda: config) - slug = rp.find_custom_provider_identity("https://api.mimo.example/v1") + slug = runtime_provider_custom.find_custom_provider_identity("https://api.mimo.example/v1") assert slug == "custom:mimo-v2.5-pro" - entry = rp._get_named_custom_provider(slug) + entry = runtime_provider_custom._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" diff --git a/tests/hermes_cli/test_custom_provider_normalize_no_mutate.py b/tests/hermes_cli/test_custom_provider_normalize_no_mutate.py index 1a37cb6e07..e4deb7a082 100644 --- a/tests/hermes_cli/test_custom_provider_normalize_no_mutate.py +++ b/tests/hermes_cli/test_custom_provider_normalize_no_mutate.py @@ -8,10 +8,8 @@ caller's entry, corrupting the shared cache for every subsequent reader. import copy -from hermes_cli.config import ( - _normalize_custom_provider_entry, - get_compatible_custom_providers, - providers_dict_to_custom_providers, +from hermes_cli.config_providers import ( + _normalize_custom_provider_entry, get_compatible_custom_providers, providers_dict_to_custom_providers ) diff --git a/tests/hermes_cli/test_custom_provider_tls.py b/tests/hermes_cli/test_custom_provider_tls.py index 5c1270bc2b..596138396e 100644 --- a/tests/hermes_cli/test_custom_provider_tls.py +++ b/tests/hermes_cli/test_custom_provider_tls.py @@ -1,9 +1,6 @@ """Tests for per-provider TLS settings in custom_providers config.""" -from hermes_cli.config import ( - apply_custom_provider_tls_to_client_kwargs, - get_custom_provider_tls_settings, -) +from hermes_cli.config_providers import apply_custom_provider_tls_to_client_kwargs, get_custom_provider_tls_settings def test_get_custom_provider_tls_settings_matches_base_url(): diff --git a/tests/hermes_cli/test_discord_skill_clamp_warning.py b/tests/hermes_cli/test_discord_skill_clamp_warning.py index 8ea645a7bb..dcf88e4063 100644 --- a/tests/hermes_cli/test_discord_skill_clamp_warning.py +++ b/tests/hermes_cli/test_discord_skill_clamp_warning.py @@ -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 import discord_skill_commands_by_category + from hermes_cli.commands_platforms 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 import discord_skill_commands_by_category + from hermes_cli.commands_platforms import discord_skill_commands_by_category skills_dir = tmp_path / "skills" skills_dir.mkdir() diff --git a/tests/hermes_cli/test_keyed_provider_credential_pool.py b/tests/hermes_cli/test_keyed_provider_credential_pool.py index e059ed7542..dbadc2cce7 100644 --- a/tests/hermes_cli/test_keyed_provider_credential_pool.py +++ b/tests/hermes_cli/test_keyed_provider_credential_pool.py @@ -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 import _get_named_custom_provider + from hermes_cli.runtime_provider_custom import _get_named_custom_provider entry = _get_named_custom_provider("b-ai") assert entry is not None diff --git a/tests/hermes_cli/test_meta_prompt_cache.py b/tests/hermes_cli/test_meta_prompt_cache.py index 662088b2a4..17f8f3f34f 100644 --- a/tests/hermes_cli/test_meta_prompt_cache.py +++ b/tests/hermes_cli/test_meta_prompt_cache.py @@ -78,7 +78,7 @@ class TestHostMandatedMetaResponses: class TestMetaConfigRoundtrip: def test_providers_meta_api_mode_roundtrip(self): - from hermes_cli.config import _normalize_custom_provider_entry + from hermes_cli.config_providers 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) diff --git a/tests/hermes_cli/test_model_switch_custom_providers.py b/tests/hermes_cli/test_model_switch_custom_providers.py index 259302d211..6ecd56a4d5 100644 --- a/tests/hermes_cli/test_model_switch_custom_providers.py +++ b/tests/hermes_cli/test_model_switch_custom_providers.py @@ -18,6 +18,7 @@ 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 = { @@ -2189,7 +2190,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_mod._normalize_custom_provider_entry(dict(legacy_entry)) + normalized = config_providers._normalize_custom_provider_entry(dict(legacy_entry)) assert normalized is not None assert normalized["models_discovered"] is True assert list(normalized["models"]) == _LOCAL_CATALOG diff --git a/tests/hermes_cli/test_path_completion.py b/tests/hermes_cli/test_path_completion.py index 76a2721707..5ebd9163a0 100644 --- a/tests/hermes_cli/test_path_completion.py +++ b/tests/hermes_cli/test_path_completion.py @@ -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 import SlashCommandCompleter, _file_size_label +from hermes_cli.commands_completion import SlashCommandCompleter, _file_size_label def _display_names(completions): diff --git a/tests/hermes_cli/test_plugin_capabilities.py b/tests/hermes_cli/test_plugin_capabilities.py index 2518b304fc..72679c439a 100644 --- a/tests/hermes_cli/test_plugin_capabilities.py +++ b/tests/hermes_cli/test_plugin_capabilities.py @@ -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 import parse_manifest_file + from hermes_cli.plugins_manifest 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 import parse_manifest_file + from hermes_cli.plugins_manifest import parse_manifest_file plugin_dir = tmp_path / "plainplug" plugin_dir.mkdir() diff --git a/tests/hermes_cli/test_plugin_event_bus.py b/tests/hermes_cli/test_plugin_event_bus.py index f02454bc1a..e8502bccde 100644 --- a/tests/hermes_cli/test_plugin_event_bus.py +++ b/tests/hermes_cli/test_plugin_event_bus.py @@ -21,12 +21,8 @@ import threading import pytest -from hermes_cli.plugins import ( - _EVENT_EMIT_DEPTH_CAP, - PluginContext, - PluginManager, - PluginManifest, -) +from hermes_cli.plugins import PluginContext, PluginManager, PluginManifest +from hermes_cli.plugins_dispatch import _EVENT_EMIT_DEPTH_CAP # ── Fixtures ───────────────────────────────────────────────────────────────── @@ -411,7 +407,7 @@ def test_manifest_parse_reads_emits_listens(tmp_path): encoding="utf-8", ) - from hermes_cli.plugins import parse_manifest_file + from hermes_cli.plugins_manifest import parse_manifest_file manifest = parse_manifest_file(manifest_file, plugin_dir, "user", "") assert manifest is not None @@ -429,7 +425,7 @@ def test_manifest_parse_absent_emits_listens(tmp_path): yaml.safe_dump({"name": "bare"}), encoding="utf-8" ) - from hermes_cli.plugins import parse_manifest_file + from hermes_cli.plugins_manifest import parse_manifest_file manifest = parse_manifest_file(manifest_file, plugin_dir, "user", "") assert manifest is not None diff --git a/tests/hermes_cli/test_plugin_manifest_v2.py b/tests/hermes_cli/test_plugin_manifest_v2.py index 3e8a0d9d9f..a11e1c7f70 100644 --- a/tests/hermes_cli/test_plugin_manifest_v2.py +++ b/tests/hermes_cli/test_plugin_manifest_v2.py @@ -11,13 +11,8 @@ import logging import pytest import yaml -from hermes_cli.plugins import ( - PluginManager, - PluginManifest, - SUPPORTED_MANIFEST_VERSION, - resolve_plugin_load_order, - validate_config_schema, -) +from hermes_cli.plugins import PluginManager, PluginManifest, resolve_plugin_load_order +from hermes_cli.plugins_manifest import SUPPORTED_MANIFEST_VERSION, validate_config_schema def _write_plugin(base, name, manifest_extra=None, register_body="pass"): diff --git a/tests/hermes_cli/test_plugin_prompt_sections.py b/tests/hermes_cli/test_plugin_prompt_sections.py index cebfaa2fbb..03ba118d13 100644 --- a/tests/hermes_cli/test_plugin_prompt_sections.py +++ b/tests/hermes_cli/test_plugin_prompt_sections.py @@ -5,12 +5,8 @@ from types import MappingProxyType import pytest -from hermes_cli.plugins import ( - MAX_SYSTEM_PROMPT_SECTIONS_TOTAL_CHARS, - PluginContext, - PluginManager, - PluginManifest, -) +from hermes_cli.plugins import PluginContext, PluginManager, PluginManifest +from hermes_cli.plugins_dispatch import MAX_SYSTEM_PROMPT_SECTIONS_TOTAL_CHARS def _context(manager: PluginManager, name: str = "example-plugin") -> PluginContext: diff --git a/tests/hermes_cli/test_plugins.py b/tests/hermes_cli/test_plugins.py index d0bb39e53e..20ae1f5ea0 100644 --- a/tests/hermes_cli/test_plugins.py +++ b/tests/hermes_cli/test_plugins.py @@ -12,20 +12,12 @@ import pytest import yaml from hermes_cli.plugins import ( - 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, + 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 ) +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, @@ -1148,10 +1140,8 @@ class TestForceReloadSymmetry: """Timed-out pre_tool_call must return a block directive, not allow.""" import time - from hermes_cli.plugins import ( - _PRE_TOOL_CALL_TIMEOUT_BLOCK_MESSAGE, - resolve_pre_tool_block, - ) + from hermes_cli.plugins import resolve_pre_tool_block + from hermes_cli.plugins_dispatch import _PRE_TOOL_CALL_TIMEOUT_BLOCK_MESSAGE monkeypatch.setattr( "hermes_cli.plugins._resolve_hook_callback_timeout", lambda: 0.1 @@ -1186,7 +1176,7 @@ class TestForceReloadSymmetry: """E2E: timed-out pre_tool_call blocks handle_function_call before dispatch.""" import json - from hermes_cli.plugins import _PRE_TOOL_CALL_TIMEOUT_BLOCK_MESSAGE + from hermes_cli.plugins_dispatch import _PRE_TOOL_CALL_TIMEOUT_BLOCK_MESSAGE monkeypatch.setattr( "hermes_cli.plugins._resolve_hook_callback_timeout", lambda: 0.1 @@ -1281,7 +1271,8 @@ class TestPreToolCallBlocking: "hermes_cli.plugins.invoke_hook", lambda hook_name, **kwargs: [{"action": "block", "message": "blocked by plugin"}], ) - assert get_pre_tool_call_block_message("todo", {}, task_id="t1") == "blocked by plugin" + details = _get_pre_tool_call_directive_details("todo", {}, task_id="t1") + assert (details.action, details.message) == ("block", "blocked by plugin") class TestPreToolCallDirective: @@ -1289,7 +1280,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 + from hermes_cli.plugins import _get_pre_tool_call_directive_details observed = [] monkeypatch.setattr( @@ -1302,13 +1293,14 @@ class TestPreToolCallDirective: lambda hook_name, **kwargs: [], ) - assert get_pre_tool_call_directive( + details = _get_pre_tool_call_directive_details( "write_file", {"path": "README.md"}, task_id="task-1", session_id="session-1", tool_call_id="call-1", - ) == (None, None) + ) + assert (details.action, details.message) == (None, None) assert observed == [ ( "pre_tool_call", @@ -1326,24 +1318,25 @@ class TestPreToolCallDirective: ] def test_approve_directive_returned(self, monkeypatch): - from hermes_cli.plugins import get_pre_tool_call_directive + from hermes_cli.plugins import _get_pre_tool_call_directive_details monkeypatch.setattr( "hermes_cli.plugins.invoke_hook", lambda hook_name, **kwargs: [ {"action": "approve", "message": "needs human ok"} ], ) - assert get_pre_tool_call_directive("write_file", {}) == ( - "approve", "needs human ok") + details = _get_pre_tool_call_directive_details("write_file", {}) + assert (details.action, details.message) == ("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 + from hermes_cli.plugins import _get_pre_tool_call_directive_details monkeypatch.setattr( "hermes_cli.plugins.invoke_hook", lambda hook_name, **kwargs: [{"action": "approve"}], ) - assert get_pre_tool_call_directive("write_file", {}) == ("approve", None) + details = _get_pre_tool_call_directive_details("write_file", {}) + assert (details.action, details.message) == ("approve", None) class TestResolvePreToolBlock: @@ -1577,7 +1570,7 @@ class TestThreadToolWhitelist: ) set_thread_tool_whitelist({"memory", "skill_manage"}) try: - assert get_pre_tool_call_block_message("memory", {}) is None + assert _get_pre_tool_call_directive_details("memory", {}).action != "block" finally: clear_thread_tool_whitelist() @@ -1596,7 +1589,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_block_message("terminal", {}) is None + assert _get_pre_tool_call_directive_details("terminal", {}).action != "block" def test_whitelist_is_thread_local(self, monkeypatch): """Setting a whitelist in one thread must NOT leak into another.""" @@ -1615,18 +1608,18 @@ class TestThreadToolWhitelist: # Main thread: install a restrictive whitelist. set_thread_tool_whitelist({"memory"}) try: - assert get_pre_tool_call_block_message("terminal", {}) is not None + assert _get_pre_tool_call_directive_details("terminal", {}).action == "block" # Worker thread: should NOT inherit main thread's whitelist. result = {} def worker(): - result["msg"] = get_pre_tool_call_block_message("terminal", {}) + result["msg"] = _get_pre_tool_call_directive_details("terminal", {}).action t = threading.Thread(target=worker) t.start() t.join() - assert result["msg"] is None, ( + assert result["msg"] != "block", ( "thread-local whitelist leaked across threads" ) finally: diff --git a/tests/hermes_cli/test_provider_config_validation.py b/tests/hermes_cli/test_provider_config_validation.py index 892a8876c6..99399f5541 100644 --- a/tests/hermes_cli/test_provider_config_validation.py +++ b/tests/hermes_cli/test_provider_config_validation.py @@ -8,10 +8,7 @@ import logging import pytest -from hermes_cli.config import ( - _PROVIDER_NORMALIZE_WARNED, - _normalize_custom_provider_entry, -) +from hermes_cli.config_providers import _PROVIDER_NORMALIZE_WARNED, _normalize_custom_provider_entry class TestNormalizeCustomProviderEntry: @@ -70,7 +67,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 import find_provider_entry, stringify_provider_map + from hermes_cli.config_providers import find_provider_entry, stringify_provider_map result = _normalize_custom_provider_entry( {"name": 2070, "base_url": "http://192.168.1.10:8082/v1"}, diff --git a/tests/hermes_cli/test_resolve_ephemeral_system_prompt.py b/tests/hermes_cli/test_resolve_ephemeral_system_prompt.py index 6a1ac5f326..8e7d60f22b 100644 --- a/tests/hermes_cli/test_resolve_ephemeral_system_prompt.py +++ b/tests/hermes_cli/test_resolve_ephemeral_system_prompt.py @@ -1,9 +1,6 @@ -"""Unit tests for resolve_ephemeral_system_prompt_from_config.""" +"""Unit tests for resolve_ephemeral_system_prompt.""" -from hermes_cli.config import ( - render_personality_prompt, - resolve_ephemeral_system_prompt_from_config, -) +from hermes_cli.personality import render_personality_prompt, resolve_ephemeral_system_prompt def test_resolve_uses_named_personality_when_set(): @@ -14,7 +11,7 @@ def test_resolve_uses_named_personality_when_set(): "personalities": {"helpful": "You are helpful."}, }, } - assert resolve_ephemeral_system_prompt_from_config(cfg) == "You are helpful." + assert resolve_ephemeral_system_prompt(cfg) == "You are helpful." def test_resolve_falls_back_to_manual_system_prompt(): @@ -25,7 +22,7 @@ def test_resolve_falls_back_to_manual_system_prompt(): "personalities": {"helpful": "You are helpful."}, }, } - assert resolve_ephemeral_system_prompt_from_config(cfg) == "manual forever" + assert resolve_ephemeral_system_prompt(cfg) == "manual forever" def test_resolve_ignores_unknown_personality_name(): @@ -36,7 +33,7 @@ def test_resolve_ignores_unknown_personality_name(): "personalities": {"helpful": "You are helpful."}, }, } - assert resolve_ephemeral_system_prompt_from_config(cfg) == "manual forever" + assert resolve_ephemeral_system_prompt(cfg) == "manual forever" def test_resolve_renders_dict_personality(): @@ -53,7 +50,7 @@ def test_resolve_renders_dict_personality(): }, }, } - resolved = resolve_ephemeral_system_prompt_from_config(cfg) + resolved = resolve_ephemeral_system_prompt(cfg) assert "You are an expert programmer." in resolved assert "Tone: technical" in resolved assert "Style: concise" in resolved diff --git a/tests/hermes_cli/test_runtime_provider_late_binding.py b/tests/hermes_cli/test_runtime_provider_late_binding.py index 1212a34ca3..65096e8a9a 100644 --- a/tests/hermes_cli/test_runtime_provider_late_binding.py +++ b/tests/hermes_cli/test_runtime_provider_late_binding.py @@ -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.get_compatible_custom_providers", return_value=marker + "hermes_cli.config_providers.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.normalize_extra_headers", return_value=hdrs): + with patch("hermes_cli.config_providers.normalize_extra_headers", return_value=hdrs): assert rp.normalize_extra_headers(None) is hdrs assert rp.normalize_extra_headers(None) is not hdrs diff --git a/tests/hermes_cli/test_runtime_provider_resolution.py b/tests/hermes_cli/test_runtime_provider_resolution.py index 35865157c6..500ac91fdd 100644 --- a/tests/hermes_cli/test_runtime_provider_resolution.py +++ b/tests/hermes_cli/test_runtime_provider_resolution.py @@ -6,6 +6,8 @@ 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): @@ -115,19 +117,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(rp, "custom_provider_pool_key_candidates", lambda base_url, provider_name=None: ["custom:local-ollama"]) + monkeypatch.setattr(credential_pool, "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 = rp._try_resolve_from_custom_pool("http://localhost:11434/v1", "custom", None) + result = runtime_provider_custom._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(rp, "custom_provider_pool_key_candidates", lambda base_url, provider_name=None: ["custom:local"]) + monkeypatch.setattr(credential_pool, "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 = rp._try_resolve_from_custom_pool("http://127.0.0.1:11434/v1", "custom", None) + result = runtime_provider_custom._try_resolve_from_custom_pool("http://127.0.0.1:11434/v1", "custom", None) assert result["api_key"] == "no-key-required" @@ -136,10 +138,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(rp, "custom_provider_pool_key_candidates", lambda base_url, provider_name=None: ["custom:remote"]) + monkeypatch.setattr(credential_pool, "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 = rp._try_resolve_from_custom_pool("https://api.remote-vendor.example/v1", "custom", None) + result = runtime_provider_custom._try_resolve_from_custom_pool("https://api.remote-vendor.example/v1", "custom", None) assert result["api_key"] == "xy" @@ -147,10 +149,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(rp, "custom_provider_pool_key_candidates", lambda base_url, provider_name=None: ["custom:local"]) + monkeypatch.setattr(credential_pool, "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 = rp._try_resolve_from_custom_pool("http://localhost:11434/v1", "custom", None) + result = runtime_provider_custom._try_resolve_from_custom_pool("http://localhost:11434/v1", "custom", None) assert result["api_key"] == "sk-genuinely-long-real-key-12345" @@ -687,7 +689,7 @@ def test_named_custom_provider_filters_capabilities_at_lookup_boundary(monkeypat ), ) - provider = rp._get_named_custom_provider("local") + provider = runtime_provider_custom._get_named_custom_provider("local") assert provider["capabilities"] == {"openai_native_compaction": True} @@ -842,7 +844,7 @@ def test_named_custom_provider_wins_over_builtin_alias(monkeypatch): }, ) - entry = rp._get_named_custom_provider("kimi") + entry = runtime_provider_custom._get_named_custom_provider("kimi") assert entry is not None assert entry["base_url"] == "https://my-custom-kimi.example.com/v1" @@ -1000,7 +1002,7 @@ class TestOllamaUrlSubstringLeak: "http://127.0.0.1:9000/ollama.com/v1" )) monkeypatch.setattr(rp, "load_pool", lambda provider: None) - monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None) + monkeypatch.setattr(runtime_provider_custom, "_try_resolve_from_custom_pool", lambda *a, **k: None) resolved = rp.resolve_runtime_provider(requested="custom") @@ -1021,7 +1023,7 @@ class TestOllamaUrlSubstringLeak: "http://ollama.com.attacker.test:9000/v1" )) monkeypatch.setattr(rp, "load_pool", lambda provider: None) - monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None) + monkeypatch.setattr(runtime_provider_custom, "_try_resolve_from_custom_pool", lambda *a, **k: None) resolved = rp.resolve_runtime_provider(requested="custom") @@ -1039,7 +1041,7 @@ class TestOllamaUrlSubstringLeak: "https://ollama.com/v1" )) monkeypatch.setattr(rp, "load_pool", lambda provider: None) - monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None) + monkeypatch.setattr(runtime_provider_custom, "_try_resolve_from_custom_pool", lambda *a, **k: None) resolved = rp.resolve_runtime_provider(requested="custom") @@ -1215,7 +1217,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 import _normalize_custom_provider_entry + from hermes_cli.config_providers import _normalize_custom_provider_entry entry = { "name": "vendor", "base_url": "https://api.vendor.example.com/v1", @@ -1233,10 +1235,8 @@ 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, - _normalize_custom_provider_entry, - ) + from hermes_cli.config import _VALID_CUSTOM_PROVIDER_FIELDS + from hermes_cli.config_providers import _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(rp, "_try_resolve_from_custom_pool", lambda *a, **k: pool_return_value) + monkeypatch.setattr(runtime_provider_custom, "_try_resolve_from_custom_pool", lambda *a, **k: pool_return_value) monkeypatch.setattr( - rp, + runtime_provider_custom, "_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( - rp, + runtime_provider_custom, "_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( - rp, + runtime_provider_custom, "_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( - rp, + runtime_provider_custom, "_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( - rp, + runtime_provider_custom, "_get_named_custom_provider", lambda p: { "name": "myproxy", diff --git a/tests/hermes_cli/test_secrets_bitwarden_non_tty.py b/tests/hermes_cli/test_secrets_bitwarden_non_tty.py index 5ada4d0d5c..c001af9180 100644 --- a/tests/hermes_cli/test_secrets_bitwarden_non_tty.py +++ b/tests/hermes_cli/test_secrets_bitwarden_non_tty.py @@ -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( - "hermes_cli.secrets_cli.bw.find_bws", lambda install_if_missing=False: "/usr/bin/bws" + "agent.secret_sources.bitwarden.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( - "hermes_cli.secrets_cli.bw.find_bws", lambda install_if_missing=False: "/usr/bin/bws" + "agent.secret_sources.bitwarden.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( - "hermes_cli.secrets_cli.bw.fetch_bitwarden_secrets", + "agent.secret_sources.bitwarden.fetch_bitwarden_secrets", lambda **kw: ({"KEY": "val"}, []), ) diff --git a/tests/hermes_cli/test_secrets_token_rotation.py b/tests/hermes_cli/test_secrets_token_rotation.py index 96c1bf88d1..f0702e9c58 100644 --- a/tests/hermes_cli/test_secrets_token_rotation.py +++ b/tests/hermes_cli/test_secrets_token_rotation.py @@ -14,6 +14,7 @@ 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 # --------------------------------------------------------------------------- @@ -45,7 +46,7 @@ def bw_env(monkeypatch, tmp_path): ) monkeypatch.setattr(bw_cli, "get_env_path", lambda: tmp_path / ".env") monkeypatch.setattr( - bw_cli.bw, "find_bws", + bw_backend, "find_bws", lambda install_if_missing=True: Path("/fake/bws"), ) return saved @@ -56,7 +57,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_cli.bw, "clear_caches", lambda *a, **kw: None) + monkeypatch.setattr(bw_backend, "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() diff --git a/tests/run_agent/test_63425_credential_pool_auto_detect.py b/tests/run_agent/test_63425_credential_pool_auto_detect.py index 3f78043cc8..38ee4d550e 100644 --- a/tests/run_agent/test_63425_credential_pool_auto_detect.py +++ b/tests/run_agent/test_63425_credential_pool_auto_detect.py @@ -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.get_compatible_custom_providers', return_value=[]), \ + patch('hermes_cli.config_providers.get_compatible_custom_providers', return_value=[]), \ patch('agent.iteration_budget.IterationBudget'), \ patch('hermes_cli.config.cfg_get', return_value=None): diff --git a/tests/run_agent/test_anthropic_prompt_cache_policy.py b/tests/run_agent/test_anthropic_prompt_cache_policy.py index abe95d18b7..d7180ac26c 100644 --- a/tests/run_agent/test_anthropic_prompt_cache_policy.py +++ b/tests/run_agent/test_anthropic_prompt_cache_policy.py @@ -357,7 +357,7 @@ class TestCustomProviderOpenAIWireCapability: pytest.fail("unrelated built-in route performed custom capability lookup") monkeypatch.setattr( - "hermes_cli.config.get_custom_provider_model_capability", + "hermes_cli.config_providers.get_custom_provider_model_capability", unexpected_lookup, ) diff --git a/tests/run_agent/test_credential_rotation_route_settings.py b/tests/run_agent/test_credential_rotation_route_settings.py index b11cf1d0ea..33284e6ee4 100644 --- a/tests/run_agent/test_credential_rotation_route_settings.py +++ b/tests/run_agent/test_credential_rotation_route_settings.py @@ -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.get_compatible_custom_providers", + "hermes_cli.config_providers.get_compatible_custom_providers", return_value=config["custom_providers"], ), ): diff --git a/tests/run_agent/test_env_credential_turn_refresh.py b/tests/run_agent/test_env_credential_turn_refresh.py index 078baeb8e6..b95f9b0983 100644 --- a/tests/run_agent/test_env_credential_turn_refresh.py +++ b/tests/run_agent/test_env_credential_turn_refresh.py @@ -23,6 +23,7 @@ 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" @@ -237,7 +238,7 @@ def named_custom_provider(monkeypatch): import hermes_cli.runtime_provider as rp monkeypatch.setattr( - rp, + runtime_provider_custom, "_get_named_custom_provider", lambda requested: block if requested == "longcat" else None, ) diff --git a/tests/run_agent/test_switch_model_context.py b/tests/run_agent/test_switch_model_context.py index f7dff8d633..71ee6141cd 100644 --- a/tests/run_agent/test_switch_model_context.py +++ b/tests/run_agent/test_switch_model_context.py @@ -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.get_compatible_custom_providers", fake_compatible) - monkeypatch.setattr("hermes_cli.config.get_custom_provider_context_length", fake_provider_context) + 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(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: diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index f32b829cd4..cef1ba1ef4 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -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.SlashCommandCompleter", + "hermes_cli.commands_completion.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.canonical_custom_identity", + "hermes_cli.runtime_provider_custom.canonical_custom_identity", canonical, ) monkeypatch.setattr( diff --git a/tests/tools/test_cronjob_tools.py b/tests/tools/test_cronjob_tools.py index 5e57014657..bf5f0a4f9a 100644 --- a/tests/tools/test_cronjob_tools.py +++ b/tests/tools/test_cronjob_tools.py @@ -8,6 +8,7 @@ from tools.cronjob_tools import ( check_cronjob_requirements, cronjob, ) +from hermes_cli import runtime_provider_custom # ========================================================================= @@ -322,9 +323,9 @@ class TestUnifiedCronjobTool: @staticmethod def _patch_named_legit(monkeypatch): import hermes_cli.runtime_provider as rp - monkeypatch.setattr(rp, "has_named_custom_provider", lambda n: True) + monkeypatch.setattr(runtime_provider_custom, "has_named_custom_provider", lambda n: True) monkeypatch.setattr( - rp, "_get_named_custom_provider", + runtime_provider_custom, "_get_named_custom_provider", lambda n: {"name": "legit", "base_url": "https://legit.example/v1", "api_key": "sk-legit"}, ) @@ -658,9 +659,9 @@ class TestValidateCronBaseUrl: @staticmethod def _patch_named_legit(monkeypatch): import hermes_cli.runtime_provider as rp - monkeypatch.setattr(rp, "has_named_custom_provider", lambda n: True) + monkeypatch.setattr(runtime_provider_custom, "has_named_custom_provider", lambda n: True) monkeypatch.setattr( - rp, "_get_named_custom_provider", + runtime_provider_custom, "_get_named_custom_provider", lambda n: {"name": "legit", "base_url": "https://legit.example/v1", "api_key": "sk-legit"}, ) diff --git a/tests/tui_gateway/test_custom_provider_session_persistence.py b/tests/tui_gateway/test_custom_provider_session_persistence.py index 00ed7fe5ec..7bd4cbf2d3 100644 --- a/tests/tui_gateway/test_custom_provider_session_persistence.py +++ b/tests/tui_gateway/test_custom_provider_session_persistence.py @@ -29,6 +29,7 @@ from unittest.mock import MagicMock, patch import hermes_cli.runtime_provider as rp from hermes_state import SessionDB +from hermes_cli import runtime_provider_custom MIMO_URL = "https://token-plan-cn.xiaomimimo.com/v1" MIMO_KEY = "sk-mimo-entry-key" @@ -110,7 +111,7 @@ def _make_agent_with_override(override, monkeypatch, config, model_cfg=None): monkeypatch.setattr(rp, "load_config", lambda: config) monkeypatch.setattr(rp, "_get_model_config", lambda: model_cfg or {}) # Keep credential-pool resolution off the developer's real HERMES home. - monkeypatch.setattr(rp, "_try_resolve_from_custom_pool", lambda *a, **k: None) + monkeypatch.setattr(runtime_provider_custom, "_try_resolve_from_custom_pool", lambda *a, **k: None) fake_cfg = {"agent": {"system_prompt": ""}, "model": {"default": "unused"}} with ( @@ -208,7 +209,7 @@ class TestBareCustomNoBaseUrlHealsFromConfig: # No base_url to reverse-lookup → must fall back to config.model.provider. assert ( - rp.canonical_custom_identity(base_url=None) + runtime_provider_custom.canonical_custom_identity(base_url=None) == "custom:mimo-v2.5-pro" ) @@ -339,7 +340,7 @@ class TestModelNameRecoversEntryIdentity: monkeypatch.setattr(rp, "load_config", lambda: ULTRA_CONFIG) assert ( - rp.find_custom_provider_identity_by_model("hermes-ultra-sft") + runtime_provider_custom.find_custom_provider_identity_by_model("hermes-ultra-sft") == "custom:hermes-ultra" ) diff --git a/tools/cronjob_job_args.py b/tools/cronjob_job_args.py index 11ad235cb7..f80552deb9 100644 --- a/tools/cronjob_job_args.py +++ b/tools/cronjob_job_args.py @@ -249,10 +249,8 @@ def _validate_cron_base_url( "base_url override requires an explicit provider. Set provider to a " "configured custom provider to use a custom endpoint.") try: - from hermes_cli.runtime_provider import ( - has_named_custom_provider, - resolve_requested_provider, - _get_named_custom_provider) + from hermes_cli.runtime_provider import resolve_requested_provider + from hermes_cli.runtime_provider_custom import has_named_custom_provider, _get_named_custom_provider from hermes_cli.auth import PROVIDER_REGISTRY from utils import base_url_host_matches, base_url_hostname except Exception: diff --git a/tui_gateway/methods_complete.py b/tui_gateway/methods_complete.py index ab238e4027..a7e5decc11 100644 --- a/tui_gateway/methods_complete.py +++ b/tui_gateway/methods_complete.py @@ -226,7 +226,7 @@ def _(rid, params: dict) -> dict: text = params.get("text", "") if not text.startswith("/"): return _ok(rid, {"items": []}) - from hermes_cli.commands import SlashCommandCompleter + from hermes_cli.commands_completion import SlashCommandCompleter from prompt_toolkit.document import Document from prompt_toolkit.formatted_text import to_plain_text from agent.skill_commands import get_skill_commands diff --git a/tui_gateway/methods_complete_helpers.py b/tui_gateway/methods_complete_helpers.py index b4ea31533a..68d7a5c56c 100644 --- a/tui_gateway/methods_complete_helpers.py +++ b/tui_gateway/methods_complete_helpers.py @@ -169,7 +169,7 @@ def _model_picker_context(agent): provider, base_url, model = (getattr(agent, k, "") if agent else "" for k in ("provider", "base_url", "model")) if str(provider or "").strip().lower() == "custom": try: - from hermes_cli.runtime_provider import canonical_custom_identity + from hermes_cli.runtime_provider_custom import canonical_custom_identity provider = canonical_custom_identity( base_url=base_url or None, config_provider=ctx.current_provider, model=model or None) or provider except Exception: diff --git a/tui_gateway/model_switch.py b/tui_gateway/model_switch.py index 56c2499636..8c95cda850 100644 --- a/tui_gateway/model_switch.py +++ b/tui_gateway/model_switch.py @@ -212,7 +212,8 @@ def _apply_model_switch( # (e.g. "ollama-launch") and validate against saved model lists. user_provs = custom_provs = cfg = None with contextlib.suppress(Exception): - from hermes_cli.config import get_compatible_custom_providers, load_config + from hermes_cli.config import load_config + from hermes_cli.config_providers import get_compatible_custom_providers cfg = load_config() user_provs = cfg.get("providers") custom_provs = get_compatible_custom_providers(cfg) diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 49266de5ef..26471b6273 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -1426,7 +1426,7 @@ from hermes_state import _BARE_BILLING_PROVIDERS def _is_routable_provider(provider: str) -> bool: with contextlib.suppress(Exception): - from hermes_cli.runtime_provider import is_routable_provider + from hermes_cli.runtime_provider_custom import is_routable_provider return is_routable_provider(provider) return False @@ -1484,7 +1484,7 @@ def _stored_session_runtime_overrides(row: dict | None) -> dict: if provider and not _is_routable_provider(provider): healed = None try: - from hermes_cli.runtime_provider import canonical_custom_identity + from hermes_cli.runtime_provider_custom import canonical_custom_identity healed = canonical_custom_identity(base_url=base_url or None, model=model or None) except Exception: logger.debug("custom provider identity recovery failed", exc_info=True) @@ -1519,7 +1519,7 @@ def _runtime_model_config(agent, existing: dict | None = None) -> dict: # ``agent.provider`` resolves every named custom entry to the literal "custom", losing the entry # identity (api_key is never persisted): recover ``custom:`` from the endpoint URL. try: - from hermes_cli.runtime_provider import canonical_custom_identity + from hermes_cli.runtime_provider_custom import canonical_custom_identity provider = canonical_custom_identity(base_url=base_url, model=model or None) or provider except Exception: logger.debug("custom provider identity lookup failed", exc_info=True) @@ -2210,7 +2210,7 @@ def _resolve_agent_model_runtime(model_override, provider_override) -> tuple[str override_base_url = model_override.get("base_url") resolve_kwargs = {} if str(requested_provider or "").strip().lower() == "custom": - from hermes_cli.runtime_provider import canonical_custom_identity + from hermes_cli.runtime_provider_custom import canonical_custom_identity if recovered := canonical_custom_identity(base_url=override_base_url or None, model=model or None): requested_provider = recovered if override_base_url: @@ -2238,8 +2238,8 @@ def _resolve_agent_model_runtime(model_override, provider_override) -> tuple[str def _startup_system_prompt(cfg: dict, task_id: str) -> str: """Config ephemeral system prompt + HERMES_TUI_SKILLS preload block. Hard-fails only when EVERY requested skill is missing (cli.py parity): a typo'd name must not auto-block the Kanban task.""" - from hermes_cli.config import resolve_ephemeral_system_prompt_from_config - system_prompt = resolve_ephemeral_system_prompt_from_config(cfg) + from hermes_cli.personality import resolve_ephemeral_system_prompt + system_prompt = resolve_ephemeral_system_prompt(cfg) startup_skills = _parse_tui_skills_env() if not startup_skills: return system_prompt diff --git a/tui_gateway/session_workdir.py b/tui_gateway/session_workdir.py index 0122e5eabc..9a74867866 100644 --- a/tui_gateway/session_workdir.py +++ b/tui_gateway/session_workdir.py @@ -214,7 +214,7 @@ def _workdir_row_model_config(session: dict) -> tuple[str, dict]: # ``custom:`` identity (matches _runtime_model_config). if str(model_config.get("provider") or "").strip().lower() == "custom": try: - from hermes_cli.runtime_provider import canonical_custom_identity + from hermes_cli.runtime_provider_custom import canonical_custom_identity healed = canonical_custom_identity( base_url=model_config.get("base_url") or None, model=model_config.get("model") or row_model or None) if healed: