refactor(hermes_cli): tools_config group — dense docstring reflow (word-identical, AST-verified)

This commit is contained in:
Teknium
2026-09-02 21:31:17 -07:00
parent 316427f215
commit fe8bfc1d68
5 changed files with 85 additions and 146 deletions
+21 -44
View File
@@ -49,11 +49,9 @@ def _run_text(cmd: list, *, timeout, capture_output: bool = True, **kwargs) -> s
def _post_setup_no_window_flags(*, streams_to_console: bool = False) -> int:
"""Win32 creationflags that stop post-setup children flashing a console.
CREATE_NO_WINDOW hides console grandchildren (npm, pip, powershell) while keeping stdio inheritable
(unlike DETACHED_PROCESS). Returns 0 on POSIX. ``streams_to_console`` children are only hidden when
our own stdout is not a console, so live installer output is never swallowed.
"""
our own stdout is not a console, so live installer output is never swallowed."""
from hermes_cli._subprocess_compat import windows_hide_flags
flags = windows_hide_flags()
@@ -136,11 +134,10 @@ def _cua_driver_install_ready() -> bool:
def _pip_install(args: List[str], *, timeout: int = 300, capture_output: bool = True):
"""Install Python packages from a post-setup hook.
Order: ``uv pip install`` (fast, needs no pip in the venv), then ``python -m pip install``, then
``python -m ensurepip --upgrade`` and retry pip. The last tier exists because the Windows installer
creates the venv via ``uv venv``, which does NOT seed pip, so bare ``-m pip`` failed on fresh installs.
"""
creates the venv via ``uv venv``, which does NOT seed pip, so bare ``-m pip`` failed on fresh
installs."""
venv_root = Path(sys.executable).parent.parent
uv_env = {**os.environ, "VIRTUAL_ENV": str(venv_root)}
install_flags = _post_setup_no_window_flags(streams_to_console=not capture_output)
@@ -207,12 +204,10 @@ def _cua_driver_version(binary: str) -> Optional[str]:
def _confirmed_update_check(driver_cmd: str, require_confirmed_update: bool) -> tuple:
"""Ask the installed driver whether a newer release exists; returns ``(proceed, pin_version)``.
``proceed=False`` means stop with success (already latest, or indeterminate under
``require_confirmed_update``). An old driver (no check-update verb) or offline check yields None:
`hermes update` then keeps the installed version — an indeterminate check must never cost a
multi-minute silent reinstall on every update — while explicit `install --upgrade` falls through.
"""
multi-minute silent reinstall on every update — while explicit `install --upgrade` falls through."""
try:
from tools.computer_use.cua_backend import cua_driver_update_check
_state = cua_driver_update_check()
@@ -248,11 +243,9 @@ def install_cua_driver(
upgrade: bool = False, require_confirmed_update: bool = False, show_installer_progress: bool = True,
) -> bool:
"""Install or refresh the cua-driver binary used by Computer Use.
The upstream installer always pulls the latest release tag, so re-running it is the canonical way to
upgrade. ``upgrade=False`` (toolset enable flow) keeps a compatible installation, repairs an
old/incomplete one and installs when missing; ``upgrade=True`` always refreshes.
"""
old/incomplete one and installs when missing; ``upgrade=True`` always refreshes."""
import platform as _plat
system = _plat.system()
@@ -385,11 +378,9 @@ def _cua_windows_install_lock_file() -> "Path":
def _clear_stale_windows_cua_install_lock() -> None:
"""Delete install.ps1's lock file only when no process still holds it.
install.ps1 locks with ``FileShare::None``; mirror it with a zero-share ``CreateFileW`` probe and
``FILE_FLAG_DELETE_ON_CLOSE`` so an unlocked leftover is removed atomically, with no window in which
a new installer could acquire the file between probe and delete.
"""
a new installer could acquire the file between probe and delete."""
lock_file = _cua_windows_install_lock_file()
try:
if not lock_file.is_file():
@@ -437,11 +428,9 @@ def _clear_stale_windows_cua_install_lock() -> None:
def _clear_stale_cua_install_lock() -> None:
"""Best-effort: remove a stale installer lock left by a dead holder.
POSIX stamps the holder pid into ``~/.cua-driver/packages/.install.lock.d/info``; Windows holds
``~/.cua-driver/install.lock`` open with ``FileShare::None``. Clear either artifact up front only
when its platform-specific liveness check proves that no install still holds it.
"""
when its platform-specific liveness check proves that no install still holds it."""
if sys.platform == "win32":
_clear_stale_windows_cua_install_lock()
return
@@ -485,10 +474,8 @@ def _clear_stale_cua_install_lock() -> None:
def _cua_install_lock_held() -> bool:
"""True when the upstream installer's lock is held by a LIVE process.
Called after ``_clear_stale_cua_install_lock()``: anything provably stale is already gone, so a
surviving lock artifact means a concurrent (or orphaned-but-alive) install owns it.
"""
surviving lock artifact means a concurrent (or orphaned-but-alive) install owns it."""
try:
if sys.platform != "win32":
return _cua_install_lock_dir().is_dir()
@@ -509,11 +496,9 @@ def _cua_install_lock_held() -> bool:
def _cua_release_endpoint_reachable(timeout: float = 5.0) -> bool:
"""Fast probe: can we reach GitHub's release download host at all?
When github.com is down the installer dies slowly inside its own retries and eats the whole unattended
ceiling; a 5s HEAD decides in seconds. Only a connection-level failure counts as unreachable — any
HTTP response (even 4xx/5xx) proves the path works.
"""
When github.com is down the installer dies slowly inside its own retries and eats the whole
unattended ceiling; a 5s HEAD decides in seconds. Only a connection-level failure counts as
unreachable — any HTTP response (even 4xx/5xx) proves the path works."""
import urllib.error
import urllib.request
@@ -548,11 +533,9 @@ def _cua_driver_autostart_registered_windows() -> bool:
def _repair_cua_driver_autostart_windows(driver_cmd: str, *, verbose: bool) -> bool:
"""Best-effort repair for Windows installer autostart quoting failures.
Older install.ps1 builds interpolated the binary path into a PowerShell command string, which split at
the first space. If the scheduled task is missing, retry via Start-Process's structured ``-FilePath`` /
``-ArgumentList`` parameters instead.
"""
Older install.ps1 builds interpolated the binary path into a PowerShell command string, which split
at the first space. If the scheduled task is missing, retry via Start-Process's structured
``-FilePath`` / ``-ArgumentList`` parameters instead."""
if sys.platform != "win32" or _cua_driver_autostart_registered_windows():
return True
@@ -654,10 +637,8 @@ def _kill_installer_tree(proc, *, is_windows: bool) -> None:
def _reap_after_timeout(proc, *, is_windows: bool) -> None:
"""Kill the installer tree, then drain its pipes under a deadline.
An unbounded drain blocks on an EOF that only arrives when someone kills a surviving descendant by
hand, so ``_CUA_INSTALLER_TIMEOUT`` would stop bounding anything.
"""
hand, so ``_CUA_INSTALLER_TIMEOUT`` would stop bounding anything."""
_kill_installer_tree(proc, is_windows=is_windows)
try:
drained_out, _ = proc.communicate(timeout=_CUA_INSTALLER_DRAIN_GRACE)
@@ -705,13 +686,11 @@ def _cua_installer_command(is_windows: bool):
def _unattended_installer_preflight(install_cmd: list, is_windows: bool):
"""Fail FAST on the two conditions that otherwise consume the whole unattended ceiling.
1. Install lock held by a live process — upstream would poll it for up to LOCK_STALE_AFTER_SECONDS=600
before probing the holder (the 11-minute silent hang class).
2. Release host unreachable — the installer would die slowly inside its own retries; a 5s HEAD answers.
Returns the (possibly rewritten) install command, or None to skip this refresh. Explicit
`computer-use install --upgrade` runs never come here and keep upstream's full lock-recovery.
"""
1. Install lock held by a live process — upstream would poll it for up to
LOCK_STALE_AFTER_SECONDS=600 before probing the holder (the 11-minute silent hang class).
2. Release host unreachable — the installer would die slowly inside its own retries; a 5s HEAD
answers. Returns the (possibly rewritten) install command, or None to skip this refresh. Explicit
`computer-use install --upgrade` runs never come here and keep upstream's full lock-recovery."""
if _cua_install_lock_held():
_print_info(" Another cua-driver install is in progress (upstream install lock is held) — skipping this refresh.")
_print_info(f" If no install is really running, retry with: {_UPGRADE_CMD}")
@@ -734,11 +713,9 @@ def _run_cua_driver_installer(
label: str = "Installing", verbose: bool = True, pin_version: Optional[str] = None, show_progress: bool = True,
installer_timeout: Optional[float] = None) -> bool:
"""Run the upstream cua-driver installer for this platform.
The scripts are idempotent: they always download the latest release, so re-running on an
already-installed system performs an upgrade. ``installer_timeout`` lets quiet callers use a shorter
ceiling without weakening the explicit install path's stale-lock recovery window.
"""
ceiling without weakening the explicit install path's stale-lock recovery window."""
import platform as _plat
system = _plat.system()
+3 -5
View File
@@ -14,11 +14,9 @@ from hermes_cli.toolset_scope import (
def _mcp_match_filter():
"""Runtime name-filter matcher (exact names or fnmatch globs), with a literal fallback.
Must use the SAME semantics as tools/mcp_tool.py registration — a literal ``in`` check renders
glob excludes (e.g. ``*team_member*`` from catalog default_excluded manifests) as if nothing
were excluded.
"""
Must use the SAME semantics as tools/mcp_tool.py registration — a literal ``in`` check renders glob
excludes (e.g. ``*team_member*`` from catalog default_excluded manifests) as if nothing were
excluded."""
try:
from tools.mcp_tool import matches_name_filter
return matches_name_filter
+11 -19
View File
@@ -25,11 +25,9 @@ PROJECT_ROOT = Path(__file__).parent.parent.resolve()
def _ensure_browser_use_cli(*, verbose_hints: bool = False) -> None:
"""Install the Browser Use CLI if it isn't already runnable.
Primary driver engine for EVERY browser backend except Camofox (Firefox-based, no CDP surface).
MANAGED-FIRST: a browser-use on the user's PATH does NOT satisfy this check — only the
Hermes-managed ``$HERMES_HOME/bin`` copy does.
"""
Hermes-managed ``$HERMES_HOME/bin`` copy does."""
_print_info(" Ensuring browser-use CLI (managed install)...")
try:
from tools.browser_use_cli import install_cli
@@ -86,10 +84,8 @@ def _install_chromium(install_cmd: list[str]) -> None:
def _post_setup_agent_browser(post_setup_key: str) -> None:
"""``agent_browser`` (local Chromium) and ``browserbase`` (cloud rows) hooks.
agent-browser is not a root package.json dependency — it resolves lazily via npx (or a
global/Hermes-managed install), so there is no ``npm install`` step here.
"""
global/Hermes-managed install), so there is no ``npm install`` step here."""
# Every non-Camofox backend drives through the Browser Use CLI — install it here too.
_ensure_browser_use_cli()
try:
@@ -286,10 +282,9 @@ def _post_setup_langfuse() -> None:
def _post_setup_xai_grok() -> None:
"""Shared xAI credential bootstrap for any picker row that talks to xAI (TTS, STT, Video Gen,
x_search …). Accepts a SuperGrok-tier OAuth token (preferred — billed to the existing
subscription) or a raw XAI_API_KEY; the rows declare empty env_vars so the auth UX lives here.
"""
"""Shared xAI credential bootstrap for any picker row that talks to xAI (TTS, STT, Video Gen, x_search
…). Accepts a SuperGrok-tier OAuth token (preferred — billed to the existing subscription) or a raw
XAI_API_KEY; the rows declare empty env_vars so the auth UX lives here."""
try:
from hermes_cli.auth import get_xai_oauth_auth_status
oauth_logged_in = bool(get_xai_oauth_auth_status().get("logged_in"))
@@ -361,8 +356,7 @@ def _run_post_setup(post_setup_key: str):
def valid_post_setup_keys() -> Set[str]:
"""Return the set of post-setup keys declared by any visible provider (``TOOL_CATEGORIES`` plus
plugin-registered providers). This is the allowlist ``post-setup`` and the dashboard endpoint
validate against, so a caller cannot drive ``_run_post_setup`` with an arbitrary key.
"""
validate against, so a caller cannot drive ``_run_post_setup`` with an arbitrary key."""
from hermes_cli.tools_config import (
TOOL_CATEGORIES,
_plugin_browser_providers,
@@ -384,9 +378,8 @@ def valid_post_setup_keys() -> Set[str]:
def run_post_setup_command(args) -> int:
"""``hermes tools post-setup <key>`` — non-interactive runner the dashboard spawns so the GUI can
drive backend setup without re-implementing install logic. Exit code: 0 ok, 2 unknown key.
"""
"""``hermes tools post-setup <key>`` — non-interactive runner the dashboard spawns so the GUI can drive
backend setup without re-implementing install logic. Exit code: 0 ok, 2 unknown key."""
key = getattr(args, "post_setup_key", None)
if not key:
_print_error("Usage: hermes tools post-setup <key>")
@@ -458,10 +451,9 @@ def restorable_python_tool_dependency(name: str) -> tuple[str, tuple[str, ...]]
def _agent_browser_installed() -> bool:
"""True when everything ``_run_post_setup("agent_browser")`` installs is present: the agent-browser
CLI *and* the Chromium build it drives (or the Lightpanda engine, which needs no Chromium), so
"Run setup" flips to installed only when re-running it would be a no-op.
"""
"""True when everything ``_run_post_setup("agent_browser")`` installs is present: the agent-browser CLI
*and* the Chromium build it drives (or the Lightpanda engine, which needs no Chromium), so "Run
setup" flips to installed only when re-running it would be a no-op."""
from hermes_cli.nous_subscription import _local_browser_runnable
# The hook runs in a spawned process; this probe runs in the long-lived web-server/CLI process whose
+45 -70
View File
@@ -39,11 +39,10 @@ def _plugin_provider_rows(
registry_module: str, marker_keys: tuple[str, ...], *, require_name: bool = True, skip_builtin: bool = False,
flatten_variants: bool = False) -> list[dict]:
"""Picker-row dicts (TOOL_CATEGORIES-shaped) for a plugin registry's providers.
``marker_keys`` are all set to the registry name so downstream config/model-picker code routes through
the registry. ``skip_builtin`` drops names shadowing ``_BUILTIN_NAMES``; ``flatten_variants`` expands a
schema's tier ``variants`` into separate rows sharing one backend name, distinguished by ``web_tier``.
"""
``marker_keys`` are all set to the registry name so downstream config/model-picker code routes
through the registry. ``skip_builtin`` drops names shadowing ``_BUILTIN_NAMES``;
``flatten_variants`` expands a schema's tier ``variants`` into separate rows sharing one backend
name, distinguished by ``web_tier``."""
registry = _plugin_registry(registry_module)
if registry is None:
return []
@@ -109,10 +108,8 @@ _plugin_tts_providers = partial(_plugin_rows_for, "tts")
def web_provider_capabilities(backend: str) -> list:
"""Capabilities (``search`` / ``extract``) a web backend supports, per the registry provider instance.
Lets the Capabilities GUI offer ``web.search_backend`` / ``web.extract_backend`` only where it makes sense
(ddgs and brave-free are search-only). Unknown backend or registry failure -> both.
"""
Lets the Capabilities GUI offer ``web.search_backend`` / ``web.extract_backend`` only where it makes
sense (ddgs and brave-free are search-only). Unknown backend or registry failure -> both."""
try:
from agent.web_search_registry import get_provider
@@ -138,10 +135,8 @@ def _visible_providers(
cat: dict, config: dict, *, force_fresh: bool = False, features: Optional[NousSubscriptionFeatures] = None,
) -> list[dict]:
"""Provider entries visible for the current auth/config state.
Nous-managed rows (``managed_nous_feature``) are always shown, even logged-out/unentitled, to advertise
the capability.
"""
Nous-managed rows (``managed_nous_feature``) are always shown, even logged-out/unentitled, to
advertise the capability."""
from hermes_cli.tools_config import get_nous_subscription_features
if features is None:
@@ -170,10 +165,8 @@ def _visible_providers(
def provider_readiness_status(provider: dict, config: dict, *, features=None, is_active: Optional[bool] = None) -> str:
"""Honest readiness state for a provider picker row.
``features`` avoids re-fetching portal state per row. ``is_active`` is the completed-setup fallback for
post_setup hooks with no registered installed-check (selecting a row runs its hook).
"""
``features`` avoids re-fetching portal state per row. ``is_active`` is the completed-setup fallback
for post_setup hooks with no registered installed-check (selecting a row runs its hook)."""
from hermes_cli.tools_config import (
_POST_SETUP_READY, _provider_env_ready, _xai_credentials_present, get_nous_subscription_features,
)
@@ -262,10 +255,8 @@ def _any_plugin_provider_available(registry_module: str) -> bool:
def _configure_tool_category(ts_key: str, cat: dict, config: dict, *, force_fresh: bool = True, reconfigure: bool = False):
"""Provider selection for a tool category, then API-key setup for the chosen row.
``reconfigure`` ("Reconfigure an existing tool"): no setup note / skip row / Nous marker, and the chosen
provider goes through the key-update prompts instead of the new-enable prompts.
"""
``reconfigure`` ("Reconfigure an existing tool"): no setup note / skip row / Nous marker, and the
chosen provider goes through the key-update prompts instead of the new-enable prompts."""
from hermes_cli.tools_config import _prompt_choice, _provider_env_ready, get_nous_subscription_features
icon = cat.get("icon", "")
@@ -329,11 +320,10 @@ def _configure_tool_category(ts_key: str, cat: dict, config: dict, *, force_fres
def _web_tier_matches(provider: dict, config: dict) -> bool:
"""True when a web picker row's tier matches the configured tier (``web.provider_tier.<backend>``).
Tiered rows (Exa/Parallel Free vs Paid) share one ``web_backend`` and differ only in ``web_tier``. No
``web_tier`` on the row → matches; configured tier set → must equal the row's tier; unset → "auto":
paid when the row's key is present, free otherwise (highlight the row the runtime would actually use).
"""
Tiered rows (Exa/Parallel Free vs Paid) share one ``web_backend`` and differ only in ``web_tier``.
No ``web_tier`` on the row → matches; configured tier set → must equal the row's tier; unset →
"auto": paid when the row's key is present, free otherwise (highlight the row the runtime would
actually use)."""
row_tier = provider.get("web_tier")
if not row_tier:
return True
@@ -547,11 +537,10 @@ def _pick_model_from_catalog(
catalog: dict, default_model, cfg_key: str, display: str, config: dict, *, row_indent: str = "",
) -> None:
"""Column-aligned model picker shared by the FAL, plugin image gen and video gen flows.
Writes the choice to ``config[cfg_key]["model"]``. The current model is listed first so the cursor lands
on it; a saved model belonging to another provider (shared config key) or a drifted catalog default never
indexes the catalog. Safe when stdin is not a TTY — curses_radiolist keeps the current selection.
"""
Writes the choice to ``config[cfg_key]["model"]``. The current model is listed first so the cursor
lands on it; a saved model belonging to another provider (shared config key) or a drifted catalog
default never indexes the catalog. Safe when stdin is not a TTY — curses_radiolist keeps the current
selection."""
from hermes_cli.tools_config import _cfg_section, _prompt_choice
if not catalog:
@@ -633,10 +622,9 @@ def _configure_xai_imagine_storage(section_name: str, config: dict) -> None:
def _select_plugin_gen_provider(section: str, plugin_name: str, config: dict, *, use_gateway: bool = False) -> None:
"""Persist a plugin-backed image/video gen provider selection and run its model picker.
``use_gateway=True`` (Nous-managed pick) stores ``<section>.provider: nous``; BYOK picks store the plugin
name. Any legacy ``use_gateway`` key is removed so old read-time shims cannot override the selection.
"""
``use_gateway=True`` (Nous-managed pick) stores ``<section>.provider: nous``; BYOK picks store the
plugin name. Any legacy ``use_gateway`` key is removed so old read-time shims cannot override the
selection."""
from hermes_cli.tools_config import _cfg_section
cfg = _cfg_section(config, section)
@@ -698,12 +686,10 @@ def _drop_use_gateway(section) -> None:
def _write_provider_config(provider: dict, config: dict, *, managed_feature) -> None:
"""Persist the provider/backend config keys for a selected provider.
Pure, non-interactive core of :func:`_configure_provider` (no env prompts, post-setup hooks, Nous auth
gating or model pickers) shared by the CLI and the GUI ``PUT .../provider`` endpoint. Each pick writes
exactly ONE provider string per category (``nous`` for managed rows) and removes any legacy
``use_gateway`` key so the read-time shim cannot override the new choice.
"""
Pure, non-interactive core of :func:`_configure_provider` (no env prompts, post-setup hooks, Nous
auth gating or model pickers) shared by the CLI and the GUI ``PUT .../provider`` endpoint. Each pick
writes exactly ONE provider string per category (``nous`` for managed rows) and removes any legacy
``use_gateway`` key so the read-time shim cannot override the new choice."""
from hermes_cli.tools_config import TOOL_CATEGORIES, _cfg_section
def _set_selection(section_key: str, name_key: str, vendor_value) -> None:
@@ -797,11 +783,10 @@ def apply_provider_selection(ts_key: str, provider_name: str, config: dict) -> N
def _nous_provider_gate(provider: dict, config: dict, managed_feature, *, force_fresh: bool) -> bool:
"""Return False (after printing why) when a Nous-gated row cannot be selected.
Managed Tool Gateway rows are always listed but only *activate* with paid Nous Portal access — selecting
one runs an inline Portal login (auth + entitlement only, no inference-provider switch). Pure pre-auth UX
rows (``requires_nous_auth`` without a managed feature) keep the older logged-in + entitled gate.
"""
Managed Tool Gateway rows are always listed but only *activate* with paid Nous Portal access —
selecting one runs an inline Portal login (auth + entitlement only, no inference-provider switch).
Pure pre-auth UX rows (``requires_nous_auth`` without a managed feature) keep the older logged-in +
entitled gate."""
from hermes_cli.tools_config import get_nous_subscription_features
if managed_feature:
@@ -896,11 +881,9 @@ def _prompt_secret(
key: str, label: str, url: str, default_val: str, *, reconfigure: bool, url_label: str, strip: bool = False,
) -> bool:
"""One env-var prompt; True unless the new-enable flow skipped the key.
Reconfigure mode shows the current value and re-prompts ("Enter to keep current"); the new-enable flow
prompts with ``default_val`` visible when one exists, else as a password, and ``strip`` decides whether
whitespace is trimmed (and whitespace-only counts as skipped).
"""
Reconfigure mode shows the current value and re-prompts ("Enter to keep current"); the new-enable
flow prompts with ``default_val`` visible when one exists, else as a password, and ``strip`` decides
whether whitespace is trimmed (and whitespace-only counts as skipped)."""
if reconfigure:
existing = get_env_value(key)
if existing:
@@ -928,10 +911,8 @@ def _prompt_secret(
def _prompt_env_vars(env_vars: list, *, reconfigure: bool) -> bool:
"""Prompt for a provider's env vars; True when every key ended up configured.
Reconfigure mode re-prompts every key and always returns True; the new-enable flow keeps already-set
keys without asking and reports False on any skipped key.
"""
keys without asking and reports False on any skipped key."""
all_configured = True
for var in env_vars:
if not reconfigure and get_env_value(var["key"]):
@@ -945,10 +926,9 @@ def _prompt_env_vars(env_vars: list, *, reconfigure: bool) -> bool:
def _configure_provider(provider: dict, config: dict, *, force_fresh: bool = True, reconfigure: bool = False):
"""Configure a single provider - prompt for API keys and set config.
``reconfigure=False`` (new-enable): already-set keys are kept without asking and the post-setup hook only
runs when every key was provided. ``reconfigure=True`` re-prompts every key and always runs the hook.
"""
``reconfigure=False`` (new-enable): already-set keys are kept without asking and the post-setup hook
only runs when every key was provided. ``reconfigure=True`` re-prompts every key and always runs the
hook."""
from hermes_cli.tools_config import _run_post_setup
env_vars = provider.get("env_vars", [])
@@ -989,10 +969,9 @@ def _reconfigure_provider(provider: dict, config: dict, *, force_fresh: bool = T
def _configure_vision_backend() -> None:
"""Interactive vision-backend configuration (``auxiliary.vision.{provider,model,base_url}``).
Offers any authenticated provider + model (same surface as ``hermes model``) or a custom endpoint rather
than forcing OpenRouter. "Auto" leaves the keys empty so the resolver uses the main-model fallback chain.
"""
Offers any authenticated provider + model (same surface as ``hermes model``) or a custom endpoint
rather than forcing OpenRouter. "Auto" leaves the keys empty so the resolver uses the main-model
fallback chain."""
from hermes_cli.tools_config import _cfg_section, _prompt_choice
print()
@@ -1045,11 +1024,9 @@ def _configure_vision_backend() -> None:
def _configure_vision_provider_model(config: dict, vision_cfg: dict) -> None:
"""Provider + model picker for vision, mirroring the ``/model`` surface.
Rows come from ``build_aux_picker_rows()`` so this lists exactly what the ``hermes model`` aux-task picker
lists, including user-defined ``providers:`` / ``custom_providers:`` endpoints. Persists
``auxiliary.vision.provider`` + ``.model``.
"""
Rows come from ``build_aux_picker_rows()`` so this lists exactly what the ``hermes model`` aux-task
picker lists, including user-defined ``providers:`` / ``custom_providers:`` endpoints. Persists
``auxiliary.vision.provider`` + ``.model``."""
from hermes_cli.tools_config import _prompt_choice
try:
@@ -1108,10 +1085,8 @@ def _configure_vision_provider_model(config: dict, vision_cfg: dict) -> None:
def _configure_simple_requirements(ts_key: str, *, reconfigure: bool = False):
"""Fallback for toolsets that just need env vars (no provider selection).
Vision has its own provider/model picker — run it directly so neither flow falls back to the generic
single-key prompt (which would re-ask for OPENROUTER_API_KEY).
"""
single-key prompt (which would re-ask for OPENROUTER_API_KEY)."""
from hermes_cli.tools_config import TOOLSET_ENV_REQUIREMENTS, _toolset_has_keys, _toolset_label
if ts_key == "vision":
+5 -8
View File
@@ -33,14 +33,11 @@ def validate_platform_toolsets(
is_allowed_for_platform: Callable[[str, str], bool] = toolset_allowed_for_platform,
) -> List[str]:
"""Return human-readable warnings for a ``platform_toolsets`` mapping.
Reports: a toolset name ``is_valid_toolset`` rejects (suggesting ``hermes-<platform>`` when
that would have been valid); a non-empty mapping resolving to zero valid toolsets (agent
would start with no tools); a platform with no valid toolsets, checked per-platform because
the global net is suppressed once any platform is valid; and non-list platform values, which
fall back to the platform default. ``is_valid_toolset`` is injected so this does no registry
imports or I/O.
"""
Reports: a toolset name ``is_valid_toolset`` rejects (suggesting ``hermes-<platform>`` when that
would have been valid); a non-empty mapping resolving to zero valid toolsets (agent would start with
no tools); a platform with no valid toolsets, checked per-platform because the global net is
suppressed once any platform is valid; and non-list platform values, which fall back to the platform
default. ``is_valid_toolset`` is injected so this does no registry imports or I/O."""
warnings: List[str] = []
if not isinstance(platform_toolsets, dict) or not platform_toolsets:
return warnings