refactor(hermes_cli): provider cluster — shared _overlay_pdef, pool-select guard collapse, docstring compaction (WHY kept)

This commit is contained in:
Teknium
2026-09-02 22:14:12 -07:00
parent d211f8562c
commit e789d79cfe
4 changed files with 96 additions and 132 deletions

View File

@@ -177,12 +177,17 @@ def _models_dev_info(canonical: str, allow_network: bool = True):
return None
def get_provider(name: str, *, allow_network: bool = True) -> Optional[ProviderDef]:
"""Look up a built-in provider by id or alias.
def _overlay_pdef(canonical, ov: HermesOverlay, name, env_vars, base_url, doc, source) -> ProviderDef:
return ProviderDef(
id=canonical, name=name, transport=ov.transport, api_key_env_vars=env_vars, base_url=base_url,
base_url_env_var=ov.base_url_env_var, is_aggregator=ov.is_aggregator, auth_type=ov.auth_type, doc=doc,
source=source,
)
Order: models.dev catalog merged with the Hermes overlay; Hermes-only overlay (nous,
openai-codex, …); plugin provider profiles with a concrete endpoint.
"""
def get_provider(name: str, *, allow_network: bool = True) -> Optional[ProviderDef]:
"""Look up a built-in provider by id or alias: models.dev catalog merged with the Hermes overlay;
Hermes-only overlay (nous, openai-codex, …); plugin provider profiles with a concrete endpoint."""
canonical = normalize_provider(name)
mdev_info = _models_dev_info(canonical, allow_network)
overlay = HERMES_OVERLAYS.get(canonical)
@@ -192,17 +197,14 @@ def get_provider(name: str, *, allow_network: bool = True) -> Optional[ProviderD
for ev in ov.extra_env_vars:
if ev not in env_vars:
env_vars.append(ev)
return ProviderDef(
id=canonical, name=mdev_info.name, transport=ov.transport, api_key_env_vars=tuple(env_vars),
base_url=ov.base_url_override or mdev_info.api, base_url_env_var=ov.base_url_env_var,
is_aggregator=ov.is_aggregator, auth_type=ov.auth_type, doc=mdev_info.doc, source="models.dev",
return _overlay_pdef(
canonical, ov, mdev_info.name, tuple(env_vars), ov.base_url_override or mdev_info.api, mdev_info.doc,
"models.dev",
)
if overlay is not None:
return ProviderDef(
id=canonical, name=_LABEL_OVERRIDES.get(canonical, canonical), transport=overlay.transport,
api_key_env_vars=overlay.extra_env_vars, base_url=overlay.base_url_override,
base_url_env_var=overlay.base_url_env_var, is_aggregator=overlay.is_aggregator,
auth_type=overlay.auth_type, source="hermes",
return _overlay_pdef(
canonical, overlay, _LABEL_OVERRIDES.get(canonical, canonical), overlay.extra_env_vars,
overlay.base_url_override, "", "hermes",
)
# Plugin-registered profiles (plugins/model-providers/<name>/) absent from models.dev and
# HERMES_OVERLAYS would otherwise be "Unknown provider" in /model, --provider and model-switch
@@ -264,12 +266,10 @@ def is_routing_aggregator(provider: str) -> bool:
def is_official_openai_host(base_url: str) -> bool:
"""True when *base_url* points at OpenAI's official API host family.
Hostname-parsed matching only — never substring — so lookalike hosts
(``api.openai.com.attacker.test``) and path-segment spoofs (``proxy.test/api.openai.com/v1``)
are rejected. A genuine ``*.api.openai.com`` subdomain requires control of openai.com DNS.
"""
"""True when *base_url* points at OpenAI's official API host family. Hostname-parsed matching
only — never substring — so lookalike hosts (``api.openai.com.attacker.test``) and path-segment
spoofs (``proxy.test/api.openai.com/v1``) are rejected; a genuine ``*.api.openai.com``
subdomain requires control of openai.com DNS."""
return base_url_host_matches(base_url, "api.openai.com")
@@ -281,14 +281,12 @@ _RESPONSES_NATIVE_HOSTS: frozenset[str] = frozenset({"api.meta.ai", "api.router.
def host_mandated_api_mode(base_url: str = "") -> Optional[str]:
"""Return the wire protocol a specific endpoint *requires*, or None.
Some hosts accept exactly one API mode (api.openai.com 400s chat/completions for reasoning
models with tools). These are *mandatory*: a session carrying a stale api_mode (a /model switch
that kept the previous provider's ``chat_completions``) must be overridden, not merely filled
in when empty. Exact-hostname matching only — never substring — so lookalike hosts and
path-segment spoofs are not treated as the real endpoint.
"""
"""Return the wire protocol a specific endpoint *requires*, or None. Some hosts accept exactly
one API mode (api.openai.com 400s chat/completions for reasoning models with tools); these are
*mandatory*: a session carrying a stale api_mode (a /model switch that kept the previous
provider's ``chat_completions``) must be overridden, not merely filled in when empty.
Exact-hostname matching only — never substring — so lookalike hosts and path-segment spoofs are
not treated as the real endpoint."""
if not base_url:
return None
url_lower = base_url.rstrip("/").lower()
@@ -383,11 +381,9 @@ def custom_provider_aliases(display_name: str, provider_key: str = "") -> frozen
def resolve_custom_provider(name: str, custom_providers: Optional[List[Dict[str, Any]]]) -> Optional[ProviderDef]:
"""Resolve a provider from the user's config.yaml ``custom_providers`` list.
A stored bare ``"custom"`` (corrupt state from a prior model-switch bug) falls back to the first
valid entry so existing configs self-heal.
"""
"""Resolve a provider from the user's config.yaml ``custom_providers`` list. A stored bare
``"custom"`` (corrupt state from a prior model-switch bug) falls back to the first valid entry
so existing configs self-heal."""
if not custom_providers or not isinstance(custom_providers, list):
return None
requested = (name or "").strip().lower()
@@ -459,12 +455,9 @@ def resolve_provider_full(
) -> Optional[ProviderDef]:
"""Full resolution chain: user ``providers.<raw name>`` -> lossy-alias registry id -> built-in
(models.dev + overlays) -> user providers (canonical, then raw) -> ``custom_providers`` ->
managed llamacpp -> models.dev directly.
User-defined ``providers.<name>`` is tried FIRST on the raw (pre-alias) name: a configured
``providers.openai`` pointing at api.openai.com must not be hijacked by the legacy
"openai" -> "openrouter" alias.
"""
managed llamacpp -> models.dev directly. User-defined ``providers.<name>`` is tried FIRST on
the raw (pre-alias) name: a configured ``providers.openai`` pointing at api.openai.com must not
be hijacked by the legacy "openai" -> "openrouter" alias."""
canonical = normalize_provider(name)
raw = name.strip().lower()
if user_providers:

View File

@@ -1,10 +1,9 @@
"""Shared runtime provider resolution for CLI, gateway, cron, and helpers.
This module owns the resolution ORDER (:func:`resolve_runtime_provider`), the api_mode / base_url
helpers and the pool / OAuth / explicit paths. Custom-provider lookup lives in
:mod:`hermes_cli.runtime_provider_custom`; Azure Foundry, OpenRouter/bare-custom, Bedrock and
external-process builders in :mod:`hermes_cli.runtime_provider_backends`. Both are re-exported
here so ``hermes_cli.runtime_provider.<name>`` imports and test patches keep working."""
"""Shared runtime provider resolution for CLI, gateway, cron, and helpers: the resolution ORDER
(:func:`resolve_runtime_provider`), api_mode / base_url helpers and the pool / OAuth / explicit paths.
Custom-provider lookup lives in :mod:`hermes_cli.runtime_provider_custom`; Azure Foundry,
OpenRouter/bare-custom, Bedrock and external-process builders in
:mod:`hermes_cli.runtime_provider_backends` — both re-exported here so
``hermes_cli.runtime_provider.<name>`` imports and test patches keep working."""
from __future__ import annotations
@@ -72,12 +71,11 @@ def _resolves_to_custom(name: str) -> bool:
def _config_base_url_trustworthy_for_bare_custom(cfg_base_url: str, cfg_provider: str) -> bool:
"""Whether ``model.base_url`` may back bare ``custom`` runtime resolution.
The model picker can select Custom while ``model.provider`` still reflects a previous provider.
Non-loopback URLs are rejected unless the YAML provider is already ``custom`` or a local-server
alias (ollama/vllm/llamacpp — else a legit LAN ollama endpoint silently falls through to
OpenRouter), so a stale OpenRouter/Z.ai base_url cannot hijack local sessions."""
"""Whether ``model.base_url`` may back bare ``custom`` runtime resolution. The picker can select
Custom while ``model.provider`` still names a previous provider, so non-loopback URLs are rejected
unless the YAML provider is already ``custom`` or a local-server alias (ollama/vllm/llamacpp —
else a legit LAN ollama endpoint falls through to OpenRouter): a stale OpenRouter/Z.ai base_url
cannot hijack local sessions."""
cfg_provider_norm = (cfg_provider or "").strip().lower()
bu = (cfg_base_url or "").strip()
if not bu:
@@ -105,11 +103,10 @@ _VALID_API_MODES = {"chat_completions", "codex_responses", "anthropic_messages",
def _detect_api_mode_for_url(base_url: str) -> Optional[str]:
"""Auto-detect api_mode from the resolved base URL, or None.
Exact-hostname matches reject lookalike subdomains (api.anthropic.com.attacker.test) and
path-segment spoofing (proxy.test/api.anthropic.com/v1). Official OpenAI hosts (incl. the
data-residency us./eu. regional hosts) need Responses for GPT-5.x tool calls with reasoning."""
"""Auto-detect api_mode from the resolved base URL, or None. Exact-hostname matches reject
lookalike subdomains (api.anthropic.com.attacker.test) and path-segment spoofing
(proxy.test/api.anthropic.com/v1). Official OpenAI hosts (incl. us./eu. data-residency hosts)
need Responses for GPT-5.x tool calls with reasoning."""
normalized = (base_url or "").strip().lower().rstrip("/")
hostname = base_url_hostname(base_url)
mandated = _HOST_MANDATED_API_MODES.get(hostname)
@@ -214,7 +211,6 @@ def _configured_or_fallback_api_mode(
provider: str, model_cfg: Dict[str, Any], base_url: str, effective_model: Any, *, opencode_by_model: bool
) -> str:
"""Persisted ``model.api_mode`` when it belongs to this provider, else URL/transport fallback.
OpenCode Zen/Go serve both anthropic_messages and chat_completions models, so (when
``opencode_by_model``) their mode is always re-derived from the effective model."""
if opencode_by_model and _models.opencode_provider_family(provider) is not None:
@@ -320,10 +316,9 @@ def _host_derived_api_key(base_url: str) -> str:
def _host_gated_env_key_candidates(base_url: str, *, ollama: bool) -> list:
"""Env API keys gated on their authoritative hosts, then the host-derived ``<VENDOR>_API_KEY``.
Sending OPENAI/OPENROUTER/OLLAMA keys to an unrelated endpoint leaks credentials
(GHSA-76xc-57q6-vm5m); match on HOST, not substring. ``_host_derived_api_key`` skips OLLAMA,
so callers that want it opt in via ``ollama``."""
(GHSA-76xc-57q6-vm5m); match on HOST, not substring. ``_host_derived_api_key`` skips OLLAMA, so
callers that want it opt in via ``ollama``."""
is_openai = base_url_host_matches(base_url, "openai.com") or base_url_host_matches(base_url, "openai.azure.com")
candidates = []
if ollama:
@@ -570,14 +565,12 @@ def _resolve_from_pool(
if not (pool and pool.has_credentials()):
return None
entry = pool.select()
pool_api_key = _pool_entry_api_key(entry) if entry is not None else ""
if provider == "nous" and entry is not None:
if entry is None:
return None
pool_api_key = _pool_entry_api_key(entry)
if provider == "nous":
entry, pool_api_key = _refresh_nous_pool_entry(pool, entry, pool_api_key)
if (
entry is not None
and pool_api_key
and credential_pool_matches_provider(pool, provider, base_url=_pool_entry_base_url(entry))
):
if pool_api_key and credential_pool_matches_provider(pool, provider, base_url=_pool_entry_base_url(entry)):
return _resolve_runtime_from_pool_entry(
provider=provider, entry=entry, requested_provider=requested_provider,
model_cfg=model_cfg, pool=pool, target_model=target_model,
@@ -939,9 +932,8 @@ def resolve_runtime_provider(
*, requested: Optional[str] = None, explicit_api_key: Optional[str] = None,
explicit_base_url: Optional[str] = None, target_model: Optional[str] = None,
) -> Dict[str, Any]:
"""Resolve runtime provider credentials for agent execution.
Ladder (order is behavior — each rung returns or raises, else falls to the next):
"""Resolve runtime provider credentials for agent execution. Ladder (order is behavior — each
rung returns or raises, else falls to the next):
1. disabled-provider guard (``providers.<name>.enabled: false``)
2. requested-name shortcuts: moa, anthropic@azure, azure-foundry, vertex
3. named custom provider / llamacpp alias / bare-custom direct alias
@@ -951,9 +943,8 @@ def resolve_runtime_provider(
7. OAuth specs (nous/codex/xai/qwen; "auto" swallows AuthError and logs) → minimax-oauth
→ external-process → anthropic env → bedrock → registry api_key providers
8. OpenRouter / bare-custom fallback
target_model: overrides model_cfg["default"] when computing provider-specific api_mode
(e.g. OpenCode Zen/Go where different models route through different API surfaces)."""
target_model overrides model_cfg["default"] when computing provider-specific api_mode (e.g.
OpenCode Zen/Go where different models route through different API surfaces)."""
requested_provider = resolve_requested_provider(requested)
_raise_if_provider_disabled(requested_provider)
runtime = _resolve_requested_shortcuts(requested_provider, explicit_api_key, explicit_base_url, target_model)

View File

@@ -1,10 +1,8 @@
"""Provider-specific runtime builders for :mod:`hermes_cli.runtime_provider`: Azure Foundry, the
OpenRouter / bare-custom fallback resolver, Bedrock, and external-process providers.
Origin-internal collaborators are resolved on the origin module at call time via :func:`_rp` so
test patches on ``hermes_cli.runtime_provider.*`` (``_get_model_config``, ``load_config``,
``has_usable_secret``, ``_try_resolve_from_custom_pool``, …) still apply.
"""
OpenRouter / bare-custom fallback resolver, Bedrock, and external-process providers. Origin-internal
collaborators are resolved on the origin module at call time via :func:`_rp` so test patches on
``hermes_cli.runtime_provider.*`` (``_get_model_config``, ``load_config``, ``has_usable_secret``,
``_try_resolve_from_custom_pool``, …) still apply."""
from __future__ import annotations
@@ -120,13 +118,11 @@ def _resolve_azure_foundry_runtime(
def _resolve_openrouter_runtime(
*, requested_provider: str, explicit_api_key: Optional[str] = None, explicit_base_url: Optional[str] = None
) -> Dict[str, Any]:
"""Terminal resolver: OpenRouter, or a bare/aliased ``custom`` endpoint.
base_url precedence: explicit > CUSTOM_BASE_URL > trusted ``model.base_url`` > OPENROUTER_BASE_URL
> default. OPENAI_BASE_URL is deliberately NOT consulted — config.yaml is the single source of
truth for endpoint URLs. OpenRouter contexts prefer OPENROUTER_API_KEY; custom endpoints never
receive the OpenRouter key and only get env keys gated on their authoritative hosts.
"""
"""Terminal resolver: OpenRouter, or a bare/aliased ``custom`` endpoint. base_url precedence:
explicit > CUSTOM_BASE_URL > trusted ``model.base_url`` > OPENROUTER_BASE_URL > default.
OPENAI_BASE_URL is deliberately NOT consulted — config.yaml is the single source of truth for
endpoint URLs. OpenRouter contexts prefer OPENROUTER_API_KEY; custom endpoints never receive the
OpenRouter key and only get env keys gated on their authoritative hosts."""
rp = _rp()
model_cfg = rp._get_model_config()
cfg_base_url = model_cfg.get("base_url") if isinstance(model_cfg.get("base_url"), str) else ""

View File

@@ -1,12 +1,9 @@
"""Custom-provider resolution: ``providers:`` / ``custom_providers:`` lookup, identity
recovery, custom credential pools, and the named-custom runtime builder.
Extracted from :mod:`hermes_cli.runtime_provider`; every name here is re-exported there.
Origin-internal collaborators (``load_config``, ``_get_model_config``, ``load_pool``,
``has_usable_secret``, ``custom_provider_pool_key_candidates``, …) are looked up on the origin
module AT CALL TIME via :func:`_rp` so ``monkeypatch.setattr(runtime_provider, name, …)`` keeps
working for moved bodies.
"""
"""Custom-provider resolution: ``providers:`` / ``custom_providers:`` lookup, identity recovery,
custom credential pools, and the named-custom runtime builder. Extracted from
:mod:`hermes_cli.runtime_provider` (every name re-exported there); origin-internal collaborators
(``load_config``, ``_get_model_config``, ``load_pool``, ``has_usable_secret``, …) are looked up on
the origin module AT CALL TIME via :func:`_rp` so ``monkeypatch.setattr(runtime_provider, name, …)``
keeps working for moved bodies."""
from __future__ import annotations
@@ -98,23 +95,19 @@ def _lift_common_custom_fields(
if api_mode:
result["api_mode"] = api_mode
_lift_max_output_tokens(entry, result)
capabilities = _filter_capabilities(entry.get("capabilities"))
if capabilities:
result["capabilities"] = capabilities
_lift_model_capabilities(entry, None, result)
# ── config lookup ──────────────────────────────────────────────────────────────────────────
def _shadowed_by_builtin(requested_norm: str) -> bool:
"""Raw names map to custom providers only when they are not canonical built-ins.
Explicit ``custom:<name>`` keys always target the saved entry, and bare ``custom`` is exempt: a
user may literally name a ``providers:`` entry "custom" (returning None before the config scan
made such cron jobs fail with ``auth_unavailable``). Defer to the built-in only when the raw
name IS the canonical provider (``nous``); an entry matching merely an alias (``kimi`` →
``kimi-coding``) is the user's target.
"""
"""Raw names map to custom providers only when they are not canonical built-ins. Explicit
``custom:<name>`` keys always target the saved entry, and bare ``custom`` is exempt: a user may
literally name a ``providers:`` entry "custom" (returning None before the config scan made such
cron jobs fail with ``auth_unavailable``). Defer to the built-in only when the raw name IS the
canonical provider (``nous``); an entry matching merely an alias (``kimi`` → ``kimi-coding``)
is the user's target."""
if requested_norm == "custom" or requested_norm.startswith("custom:"):
return False
rp = _rp()
@@ -288,15 +281,13 @@ def find_custom_provider_identity_by_model(model: str) -> Optional[str]:
def canonical_custom_identity(
*, base_url: Optional[str] = None, config_provider: Optional[str] = None, model: Optional[str] = None
) -> Optional[str]:
"""Recover a routable ``custom:<name>`` identity for a bare custom provider.
Every path that persists or restores a session's provider override must run the resolved
provider through this so a bare ``"custom"`` is upgraded back to its durable ``custom:<name>``
menu key. Sources in priority order: (1) ``base_url`` reverse lookup — the one fact that always
survives the round-trip when a URL was recorded; (2) ``model`` reverse lookup
(``model``/``default_model``/``models`` catalog); (3) the configured provider (arg, then
``model.provider``, then ``HERMES_INFERENCE_PROVIDER``) when it names a real entry.
"""
"""Recover a routable ``custom:<name>`` identity for a bare custom provider. Every path that
persists or restores a session's provider override must run the resolved provider through this
so a bare ``"custom"`` is upgraded back to its durable menu key. Sources in priority order:
(1) ``base_url`` reverse lookup — the one fact that always survives the round-trip when a URL
was recorded; (2) ``model`` reverse lookup (``model``/``default_model``/``models`` catalog);
(3) the configured provider (arg, ``model.provider``, ``HERMES_INFERENCE_PROVIDER``) when it
names a real entry."""
rp = _rp()
if base_url:
identity = find_custom_provider_identity(base_url)
@@ -336,14 +327,11 @@ def canonical_custom_identity(
def is_routable_provider(provider: Optional[str]) -> bool:
"""Whether a provider name currently resolves to a routable route.
Empty/None/``auto`` is vacuously routable (agent build falls back to the configured default).
Bare ``custom`` is the resolved billing class shared by every named entry — not a routable
identity; restore paths must heal it (:func:`canonical_custom_identity`) or fall back. Anything
else is routable iff the full chain (built-in -> ``providers:`` -> ``custom_providers:`` ->
models.dev) resolves it.
"""
"""Whether a provider name currently resolves to a routable route. Empty/None/``auto`` is
vacuously routable (agent build falls back to the configured default). Bare ``custom`` is the
resolved billing class shared by every named entry — not a routable identity; restore paths
must heal it (:func:`canonical_custom_identity`) or fall back. Anything else is routable iff the
full chain (built-in -> ``providers:`` -> ``custom_providers:`` -> models.dev) resolves it."""
name = str(provider or "").strip()
if not name or name.lower() == "auto":
return True
@@ -426,11 +414,9 @@ def _apply_custom_provider_extras(
def _resolve_llamacpp_runtime(requested_provider: str, explicit_api_key: Optional[str]) -> Dict[str, Any]:
"""Managed llama.cpp runtime: the supervised (or detected external) server, or a typed error.
No server => say so and stop; falling through to the generic custom path would surface "local
server is off" as OpenRouter's baffling "401 Invalid API key". The switch's state picks the
message (server off → point at the switch; else the setup pane).
"""
message (server off → point at the switch; else the setup pane)."""
rp = _rp()
try:
from hermes_cli.local_runtime.endpoint import resolve_llamacpp_endpoint
@@ -506,11 +492,9 @@ def _resolve_named_custom_runtime(
target_model: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
"""Runtime for a llamacpp alias, a bare-custom direct alias, or a configured custom entry.
Aliases resolving to "custom" (ollama, vllm, llamacpp, …) are treated like bare ``custom``.
A llamacpp alias with no explicit base_url resolves to the managed server first; an explicit
base_url always wins.
"""
Aliases resolving to "custom" (ollama, vllm, llamacpp, …) are treated like bare ``custom``. A
llamacpp alias with no explicit base_url resolves to the managed server first; an explicit
base_url always wins."""
rp = _rp()
requested_norm = (requested_provider or "").strip().lower()
if requested_norm in _LLAMACPP_ALIASES and not explicit_base_url: