Merge branch 'simp/r3-23-g3' into simp/integration3

This commit is contained in:
Teknium
2026-09-02 22:42:14 -07:00
5 changed files with 248 additions and 392 deletions
+49 -77
View File
@@ -177,12 +177,15 @@ 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,18 +195,11 @@ 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
# even though the picker lists them. Only profiles with a concrete endpoint resolve here:
@@ -215,12 +211,10 @@ def get_provider(name: str, *, allow_network: bool = True) -> Optional[ProviderD
_prof = _profile(canonical)
if _prof is not None and (_prof.base_url or "").strip():
_api_mode_to_transport = {v: k for k, v in TRANSPORT_TO_API_MODE.items()}
return ProviderDef(
id=canonical, name=_prof.display_name or _prof.name or canonical,
transport=_api_mode_to_transport.get(_prof.api_mode, "openai_chat"),
api_key_env_vars=tuple(_prof.env_vars or ()), base_url=_prof.base_url or "",
auth_type=_prof.auth_type or "api_key", source="plugin-profile",
)
return ProviderDef(id=canonical, name=_prof.display_name or _prof.name or canonical,
transport=_api_mode_to_transport.get(_prof.api_mode, "openai_chat"),
api_key_env_vars=tuple(_prof.env_vars or ()), base_url=_prof.base_url or "",
auth_type=_prof.auth_type or "api_key", source="plugin-profile")
except Exception:
pass
return None
@@ -264,12 +258,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 +273,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()
@@ -336,10 +326,8 @@ def determine_api_mode(provider: str, base_url: str = "", model: str = "") -> st
def _user_pdef(pid: str, name: str, base_url: str, key_env: str, transport: str = "openai_chat") -> ProviderDef:
"""``source="user-config"`` ProviderDef shared by ``providers:`` and ``custom_providers:`` entries."""
return ProviderDef(
id=pid, name=name, transport=transport, api_key_env_vars=(key_env,) if key_env else (),
base_url=base_url, is_aggregator=False, auth_type="api_key", source="user-config",
)
return ProviderDef(id=pid, name=name, transport=transport, api_key_env_vars=(key_env,) if key_env else (),
base_url=base_url, is_aggregator=False, auth_type="api_key", source="user-config")
def resolve_user_provider(name: str, user_config: Dict[str, Any]) -> Optional[ProviderDef]:
@@ -349,12 +337,10 @@ def resolve_user_provider(name: str, user_config: Dict[str, Any]) -> Optional[Pr
entry = user_config.get(name)
if not isinstance(entry, dict):
return None
return _user_pdef(
name, entry.get("name", "") or name,
entry.get("api", "") or entry.get("url", "") or entry.get("base_url", "") or "",
entry.get("key_env") or entry.get("api_key_env") or "",
entry.get("transport", "openai_chat") or "openai_chat",
)
return _user_pdef(name, entry.get("name", "") or name,
entry.get("api", "") or entry.get("url", "") or entry.get("base_url", "") or "",
entry.get("key_env") or entry.get("api_key_env") or "",
entry.get("transport", "openai_chat") or "openai_chat")
def custom_provider_slug(display_name: str, provider_key: str = "") -> str:
@@ -383,11 +369,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()
@@ -402,9 +386,8 @@ def resolve_custom_provider(name: str, custom_providers: Optional[List[Dict[str,
if not display_name or not api_url:
continue
provider_key = (entry.get("provider_key") or "").strip()
pdef = _user_pdef(
custom_provider_slug(display_name, provider_key), display_name, api_url, (entry.get("key_env") or "").strip()
)
pdef = _user_pdef(custom_provider_slug(display_name, provider_key), display_name, api_url,
(entry.get("key_env") or "").strip())
if first_valid is None:
first_valid = pdef
if requested in custom_provider_aliases(display_name, provider_key):
@@ -426,11 +409,9 @@ def _lossy_alias_registry_pdef(raw: str, canonical: str) -> Optional[ProviderDef
if _pcfg is None:
return None
if sum(1 for _rid in _AUTH_PROVIDER_REGISTRY if normalize_provider(_rid) == canonical) > 1:
return ProviderDef(
id=_pcfg.id, name=_pcfg.name, transport="openai_chat",
api_key_env_vars=tuple(_pcfg.api_key_env_vars or ()), base_url=_pcfg.inference_base_url or "",
source="hermes-auth-registry",
)
return ProviderDef(id=_pcfg.id, name=_pcfg.name, transport="openai_chat",
api_key_env_vars=tuple(_pcfg.api_key_env_vars or ()), base_url=_pcfg.inference_base_url or "",
source="hermes-auth-registry")
except Exception:
pass
return None
@@ -447,24 +428,17 @@ def _llamacpp_pdef() -> Optional[ProviderDef]:
endpoint = None
if not endpoint:
return None
return ProviderDef(
id="llamacpp", name="Local", transport="openai_chat", api_key_env_vars=(), base_url=endpoint["base_url"],
source="local-runtime",
)
return ProviderDef(id="llamacpp", name="Local", transport="openai_chat", api_key_env_vars=(), base_url=endpoint["base_url"],
source="local-runtime")
def resolve_provider_full(
name: str, user_providers: Optional[Dict[str, Any]] = None,
custom_providers: Optional[List[Dict[str, Any]]] = None,
) -> Optional[ProviderDef]:
def resolve_provider_full(name: str, user_providers: Optional[Dict[str, Any]] = None,
custom_providers: Optional[List[Dict[str, Any]]] = None) -> 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:
@@ -493,10 +467,8 @@ def resolve_provider_full(
try:
mdev_info = _models_dev_info(canonical)
if mdev_info is not None:
return ProviderDef(
id=canonical, name=mdev_info.name, transport="openai_chat", api_key_env_vars=mdev_info.env,
base_url=mdev_info.api, source="models.dev",
)
return ProviderDef(id=canonical, name=mdev_info.name, transport="openai_chat", api_key_env_vars=mdev_info.env,
base_url=mdev_info.api, source="models.dev")
except Exception:
pass
return None
+2 -4
View File
@@ -44,10 +44,8 @@ def normalize_route_base_url(base_url: Any) -> str:
return normalized
def should_clear_context_pin(
configured_model: Any, active_model: Any, configured_base_url: Any, active_base_url: Any,
configured_provider: Any, active_provider: Any,
) -> bool:
def should_clear_context_pin(configured_model: Any, active_model: Any, configured_base_url: Any, active_base_url: Any,
configured_provider: Any, active_provider: Any) -> bool:
"""True when a configured ``model.context_length`` pin no longer matches its runtime route.
Fail-closed: any error during route comparison returns ``True`` (drop the pin) so a stale window
never silently inflates the compression threshold."""
+94 -154
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)
@@ -150,9 +147,7 @@ def _resolve_plain_custom_api_mode(model_cfg: Dict[str, Any], base_url: str) ->
configured_mode = _parse_api_mode(model_cfg.get("api_mode"))
detected_mode = _detect_api_mode_for_url(base_url)
if configured_mode == "codex_responses" and detected_mode != "codex_responses":
logger.info(
"Ignoring persisted custom api_mode=codex_responses for non-OpenAI endpoint %s", base_url or "(unknown)",
)
logger.info("Ignoring persisted custom api_mode=codex_responses for non-OpenAI endpoint %s", base_url or "(unknown)")
configured_mode = None
return configured_mode or detected_mode or "chat_completions"
@@ -210,11 +205,9 @@ def _azure_inferred_api_mode(effective_model: str, api_mode: str) -> str:
return inferred or api_mode
def _configured_or_fallback_api_mode(
provider: str, model_cfg: Dict[str, Any], base_url: str, effective_model: Any, *, opencode_by_model: bool
) -> str:
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:
@@ -222,9 +215,8 @@ def _configured_or_fallback_api_mode(
return _configured_api_mode(provider, model_cfg) or _fallback_api_mode(provider, base_url, effective_model)
def _api_key_provider_api_mode(
provider: str, model_cfg: Dict[str, Any], api_key: str, base_url: str, effective_model: Any, *, opencode_by_model: bool
) -> str:
def _api_key_provider_api_mode(provider: str, model_cfg: Dict[str, Any], api_key: str, base_url: str, effective_model: Any, *,
opencode_by_model: bool) -> str:
"""api_mode for a registry ``api_key`` provider (explicit and env/config paths)."""
if provider == "copilot":
return _copilot_runtime_api_mode(model_cfg, api_key, target_model=effective_model)
@@ -320,10 +312,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:
@@ -344,13 +335,8 @@ def _pool_entry_base_url(entry: Any) -> str:
return getattr(entry, "runtime_base_url", None) or getattr(entry, "base_url", None) or ""
def _nous_pool_state(entry: Any) -> Dict[str, Any]:
return {k: getattr(entry, k, None) for k in ("agent_key", "agent_key_expires_at", "scope")}
def _registry_base_url(provider: str) -> str:
pconfig = PROVIDER_REGISTRY.get(provider)
return pconfig.inference_base_url if pconfig else ""
def _nous_entry_key_usable(entry: Any, min_ttl: int) -> bool:
return _agent_key_is_usable({k: getattr(entry, k, None) for k in ("agent_key", "agent_key_expires_at", "scope")}, min_ttl)
def _nous_min_key_ttl() -> int:
@@ -474,7 +460,8 @@ from hermes_cli.runtime_provider_backends import ( # noqa: E402,F401
_POOL_ENTRY_SIMPLE_MODES: Dict[str, tuple] = {
"openai-codex": ("codex_responses", DEFAULT_CODEX_BASE_URL), "xai-oauth": ("codex_responses", DEFAULT_XAI_OAUTH_BASE_URL),
"qwen-oauth": ("chat_completions", DEFAULT_QWEN_BASE_URL), "openrouter": ("chat_completions", OPENROUTER_BASE_URL),
"minimax-oauth": ("anthropic_messages", lambda: _registry_base_url("minimax-oauth")), "xai": ("codex_responses", ""),
"minimax-oauth": ("anthropic_messages", lambda: getattr(PROVIDER_REGISTRY.get("minimax-oauth"), "inference_base_url", "")),
"xai": ("codex_responses", ""),
}
@@ -507,20 +494,16 @@ def _pool_entry_mode_and_url(provider, entry, model_cfg, effective_model, base_u
return _configured_or_fallback_api_mode(provider, model_cfg, base_url, effective_model, opencode_by_model=True), base_url
def _resolve_runtime_from_pool_entry(
*, provider: str, entry: PooledCredential, requested_provider: str, model_cfg: Optional[Dict[str, Any]] = None,
pool: Optional[CredentialPool] = None, target_model: Optional[str] = None,
) -> Dict[str, Any]:
def _resolve_runtime_from_pool_entry(*, provider: str, entry: PooledCredential, requested_provider: str,
model_cfg: Optional[Dict[str, Any]] = None, pool: Optional[CredentialPool] = None,
target_model: Optional[str] = None) -> Dict[str, Any]:
model_cfg = model_cfg or _get_model_config()
api_mode, base_url = _pool_entry_mode_and_url(
provider, entry, model_cfg, _effective_model(model_cfg, target_model), _pool_entry_base_url(entry).rstrip("/")
)
api_mode, base_url = _pool_entry_mode_and_url(provider, entry, model_cfg, _effective_model(model_cfg, target_model),
_pool_entry_base_url(entry).rstrip("/"))
base_url = _finalize_base_url(provider, api_mode, base_url)
api_mode = _maybe_apply_codex_app_server_runtime(provider=provider, api_mode=api_mode, model_cfg=model_cfg)
return _runtime(
provider, api_mode, base_url, _pool_entry_api_key(entry),
source=getattr(entry, "source", "pool"), credential_pool=pool, requested_provider=requested_provider,
)
return _runtime(provider, api_mode, base_url, _pool_entry_api_key(entry), source=getattr(entry, "source", "pool"),
credential_pool=pool, requested_provider=requested_provider)
def _openrouter_should_use_pool(requested_provider, model_cfg, explicit_api_key, explicit_base_url) -> bool:
@@ -528,9 +511,7 @@ def _openrouter_should_use_pool(requested_provider, model_cfg, explicit_api_key,
cfg_base_url = str(model_cfg.get("base_url") or "").strip()
env_openai_base_url = _getenv("OPENAI_BASE_URL", "").strip()
env_openrouter_base_url = _getenv("OPENROUTER_BASE_URL", "").strip()
has_custom_endpoint = bool(explicit_base_url or env_openai_base_url or env_openrouter_base_url) or bool(
cfg_base_url and _cfg_provider(model_cfg) in {"auto", "custom"}
)
has_custom_endpoint = bool(explicit_base_url or env_openai_base_url or env_openrouter_base_url) or bool(cfg_base_url and _cfg_provider(model_cfg) in {"auto", "custom"})
return requested_provider in {"openrouter", "auto"} and not has_custom_endpoint and not bool(explicit_api_key or explicit_base_url)
@@ -539,7 +520,7 @@ def _refresh_nous_pool_entry(pool: CredentialPool, entry: Any, pool_api_key: str
selection (avoids network calls in `hermes auth list`); refresh here before falling back to
singleton auth resolution. Returns (entry, pool_api_key) — key "" when still unusable."""
min_ttl = _nous_min_key_ttl()
if _agent_key_is_usable(_nous_pool_state(entry), min_ttl):
if _nous_entry_key_usable(entry, min_ttl):
return entry, pool_api_key
logger.debug("Nous pool entry agent_key expired/missing, refreshing selected pool entry")
try:
@@ -550,19 +531,17 @@ def _refresh_nous_pool_entry(pool: CredentialPool, entry: Any, pool_api_key: str
if refreshed is not None:
entry = refreshed
pool_api_key = _pool_entry_api_key(entry)
if not pool_api_key or not _agent_key_is_usable(_nous_pool_state(entry), min_ttl):
if not pool_api_key or not _nous_entry_key_usable(entry, min_ttl):
logger.debug("Nous pool entry agent_key still unavailable, falling through to runtime resolution")
pool_api_key = ""
return entry, pool_api_key
def _resolve_from_pool(
provider: str, requested_provider: str, model_cfg: Dict[str, Any], explicit_api_key, explicit_base_url, target_model
) -> Optional[Dict[str, Any]]:
def _resolve_from_pool(provider: str, requested_provider: str, model_cfg: Dict[str, Any], explicit_api_key, explicit_base_url,
target_model) -> Optional[Dict[str, Any]]:
"""Runtime from the provider's credential pool, or None to continue down the ladder."""
should_use_pool = provider != "openrouter" or _openrouter_should_use_pool(
requested_provider, model_cfg, explicit_api_key, explicit_base_url
)
should_use_pool = provider != "openrouter" or _openrouter_should_use_pool(requested_provider, model_cfg, explicit_api_key,
explicit_base_url)
try:
pool = load_pool(provider) if should_use_pool else None
except Exception:
@@ -570,18 +549,14 @@ 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))
):
return _resolve_runtime_from_pool_entry(
provider=provider, entry=entry, requested_provider=requested_provider,
model_cfg=model_cfg, pool=pool, target_model=target_model,
)
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)
return None
@@ -604,14 +579,10 @@ def _creds_fallback(api_key, explicit_base_url, base_url, expiry, expiry_key, re
def _explicit_codex(requested_provider, model_cfg, api_key, explicit_base_url, target_model):
api_key, base_url, last_refresh = _creds_fallback(
api_key, explicit_base_url, explicit_base_url or DEFAULT_CODEX_BASE_URL, None, "last_refresh",
resolve_codex_runtime_credentials,
)
return _runtime(
"openai-codex", "codex_responses", base_url, api_key,
source="explicit", last_refresh=last_refresh, requested_provider=requested_provider,
)
api_key, base_url, last_refresh = _creds_fallback(api_key, explicit_base_url, explicit_base_url or DEFAULT_CODEX_BASE_URL,
None, "last_refresh", resolve_codex_runtime_credentials)
return _runtime("openai-codex", "codex_responses", base_url, api_key, source="explicit", last_refresh=last_refresh,
requested_provider=requested_provider)
def _explicit_nous(requested_provider, model_cfg, api_key, explicit_base_url, target_model):
@@ -623,23 +594,17 @@ def _explicit_nous(requested_provider, model_cfg, api_key, explicit_base_url, ta
)
# The agent_key compatibility field is used for inference only when it holds a NAS invoke JWT;
# raw OAuth access_token fallback is handled by resolve_nous_runtime_credentials().
api_key = api_key or (
str(state.get("agent_key") or "").strip() if _agent_key_is_usable(state, _nous_min_key_ttl()) else ""
)
api_key, base_url, expires_at = _creds_fallback(
api_key, explicit_base_url, base_url, state.get("agent_key_expires_at") or state.get("expires_at"), "expires_at",
_resolve_nous_creds,
)
return _runtime(
"nous", nous_api_mode(_effective_model(model_cfg, target_model)), base_url, api_key,
source="explicit", expires_at=expires_at, requested_provider=requested_provider,
)
api_key = api_key or (str(state.get("agent_key") or "").strip() if _agent_key_is_usable(state, _nous_min_key_ttl()) else "")
api_key, base_url, expires_at = _creds_fallback(api_key, explicit_base_url, base_url,
state.get("agent_key_expires_at") or state.get("expires_at"), "expires_at",
_resolve_nous_creds)
return _runtime("nous", nous_api_mode(_effective_model(model_cfg, target_model)), base_url, api_key, source="explicit",
expires_at=expires_at, requested_provider=requested_provider)
def _explicit_azure_foundry(requested_provider, model_cfg, api_key, base_url, target_model):
return _resolve_azure_foundry_runtime(
requested_provider=requested_provider, model_cfg=model_cfg, explicit_api_key=api_key, explicit_base_url=base_url
)
return _resolve_azure_foundry_runtime(requested_provider=requested_provider, model_cfg=model_cfg, explicit_api_key=api_key,
explicit_base_url=base_url)
def _actual_local_key(provider: str, api_key: str, base_url: str) -> str:
@@ -665,9 +630,8 @@ def _explicit_api_key_provider(provider, pconfig, requested_provider, model_cfg,
base_url = creds.get("base_url", "").rstrip("/")
if provider == "actual":
base_url = normalize_actual_base_url(base_url)
api_mode = _api_key_provider_api_mode(
provider, model_cfg, api_key, base_url, target_model or model_cfg.get("default", ""), opencode_by_model=False
)
api_mode = _api_key_provider_api_mode(provider, model_cfg, api_key, base_url, target_model or model_cfg.get("default", ""),
opencode_by_model=False)
api_key = _actual_local_key(provider, api_key, base_url)
return _runtime(provider, api_mode, base_url.rstrip("/"), api_key, source="explicit", requested_provider=requested_provider)
@@ -680,10 +644,9 @@ _EXPLICIT_RESOLVERS: Dict[str, Callable[..., Dict[str, Any]]] = {
}
def _resolve_explicit_runtime(
*, provider: str, requested_provider: str, model_cfg: Dict[str, Any], explicit_api_key: Optional[str] = None,
explicit_base_url: Optional[str] = None, target_model: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
def _resolve_explicit_runtime(*, provider: str, requested_provider: str, model_cfg: Dict[str, Any],
explicit_api_key: Optional[str] = None, explicit_base_url: Optional[str] = None,
target_model: Optional[str] = None) -> Optional[Dict[str, Any]]:
explicit_api_key = str(explicit_api_key or "").strip()
explicit_base_url = str(explicit_base_url or "").strip().rstrip("/")
if not explicit_api_key and not explicit_base_url:
@@ -693,9 +656,8 @@ def _resolve_explicit_runtime(
return resolver(requested_provider, model_cfg, explicit_api_key, explicit_base_url, target_model)
pconfig = PROVIDER_REGISTRY.get(provider)
if pconfig and pconfig.auth_type == "api_key":
return _explicit_api_key_provider(
provider, pconfig, requested_provider, model_cfg, explicit_api_key, explicit_base_url, target_model
)
return _explicit_api_key_provider(provider, pconfig, requested_provider, model_cfg, explicit_api_key,
explicit_base_url, target_model)
return None
@@ -744,11 +706,9 @@ def _resolve_oauth_runtime(provider, requested_provider, model_cfg, target_model
api_mode = spec.api_mode
if callable(api_mode):
api_mode = api_mode(_effective_model(model_cfg, target_model))
return _runtime(
provider, api_mode, (creds.get("base_url") or "").rstrip("/") or spec.default_base_url,
creds.get("api_key", ""), source=creds.get("source", spec.default_source),
**{spec.expiry_key: creds.get(spec.expiry_key)}, requested_provider=requested_provider,
)
return _runtime(provider, api_mode, (creds.get("base_url") or "").rstrip("/") or spec.default_base_url,
creds.get("api_key", ""), source=creds.get("source", spec.default_source),
**{spec.expiry_key: creds.get(spec.expiry_key)}, requested_provider=requested_provider)
def _minimax_oauth_runtime(provider, requested_provider) -> Optional[Dict[str, Any]]:
@@ -756,10 +716,8 @@ def _minimax_oauth_runtime(provider, requested_provider) -> Optional[Dict[str, A
if not (pconfig and pconfig.auth_type == "oauth_minimax"):
return None
creds = auth_mod.resolve_minimax_oauth_runtime_credentials()
return _runtime(
provider, "anthropic_messages", creds["base_url"], creds["api_key"],
source=creds.get("source", "oauth"), requested_provider=requested_provider,
)
return _runtime(provider, "anthropic_messages", creds["base_url"], creds["api_key"], source=creds.get("source", "oauth"),
requested_provider=requested_provider)
# ── env/config paths for anthropic and registry api_key providers ──────────────────────────
@@ -822,9 +780,8 @@ def _api_key_provider_runtime(provider, pconfig, requested_provider, model_cfg,
base_url = _config_base_url_for_provider(model_cfg, provider) or creds.get("base_url", "").rstrip("/")
if provider == "actual":
base_url = normalize_actual_base_url(base_url)
api_mode = _api_key_provider_api_mode(
provider, model_cfg, creds.get("api_key", ""), base_url, target_model or model_cfg.get("default", ""), opencode_by_model=True
)
api_mode = _api_key_provider_api_mode(provider, model_cfg, creds.get("api_key", ""), base_url,
target_model or model_cfg.get("default", ""), opencode_by_model=True)
base_url = _finalize_base_url(provider, api_mode, base_url)
api_key = _actual_local_key(provider, creds.get("api_key", ""), base_url)
return _runtime(provider, api_mode, base_url, api_key, source=creds.get("source", "env"), requested_provider=requested_provider)
@@ -871,30 +828,21 @@ def _resolve_vertex_runtime(requested_provider: str) -> Dict[str, Any]:
def _resolve_requested_shortcuts(requested_provider, explicit_api_key, explicit_base_url, target_model) -> Optional[Dict[str, Any]]:
"""Providers decided on the REQUESTED name alone, before custom / pool / generic paths."""
if requested_provider == "moa":
return _runtime(
"moa", "chat_completions", "moa://local", "moa-virtual-provider",
source="moa-virtual-provider", requested_provider=requested_provider,
)
return _runtime("moa", "chat_completions", "moa://local", "moa-virtual-provider", source="moa-virtual-provider",
requested_provider=requested_provider)
# Azure Anthropic short-circuit: an explicit Azure endpoint with provider="anthropic" must
# bypass _resolve_named_custom_runtime (which would yield custom/chat_completions/no key).
eff_base = (explicit_base_url or "").strip()
if requested_provider == "anthropic" and base_url_host_matches(eff_base, "azure.com"):
azure_key = (
(explicit_api_key or "").strip()
or _getenv("AZURE_ANTHROPIC_KEY", "").strip()
or _getenv("ANTHROPIC_API_KEY", "").strip()
)
return _runtime(
"anthropic", "anthropic_messages", eff_base.rstrip("/"), azure_key,
source="azure-explicit", requested_provider=requested_provider,
)
azure_key = (explicit_api_key or "").strip() or _getenv("AZURE_ANTHROPIC_KEY", "").strip() or _getenv("ANTHROPIC_API_KEY", "").strip()
return _runtime("anthropic", "anthropic_messages", eff_base.rstrip("/"), azure_key, source="azure-explicit",
requested_provider=requested_provider)
# Azure Foundry resolves before the custom-runtime / pool / generic paths so its config is
# always picked up from model.base_url + model.api_mode, with or without explicit_* args.
if requested_provider == "azure-foundry":
return _resolve_azure_foundry_runtime(
requested_provider=requested_provider, model_cfg=_get_model_config(),
explicit_api_key=explicit_api_key, explicit_base_url=explicit_base_url, target_model=target_model,
)
return _resolve_azure_foundry_runtime(requested_provider=requested_provider, model_cfg=_get_model_config(),
explicit_api_key=explicit_api_key, explicit_base_url=explicit_base_url,
target_model=target_model)
if requested_provider in _VERTEX_NAMES:
return _resolve_vertex_runtime(requested_provider)
return None
@@ -915,9 +863,8 @@ def _local_endpoint_bypass(requested_provider: str, explicit_api_key, explicit_b
def _openrouter_fallback(requested_provider, explicit_api_key, explicit_base_url) -> Dict[str, Any]:
runtime = _resolve_openrouter_runtime(
requested_provider=requested_provider, explicit_api_key=explicit_api_key, explicit_base_url=explicit_base_url
)
runtime = _resolve_openrouter_runtime(requested_provider=requested_provider, explicit_api_key=explicit_api_key,
explicit_base_url=explicit_base_url)
runtime["requested_provider"] = requested_provider
return runtime
@@ -935,13 +882,10 @@ def _opencode_free_runtime(provider, requested_provider, model_cfg, target_model
return free_runtime
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):
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):
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,18 +895,15 @@ 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)
if runtime:
return runtime
runtime = _resolve_named_custom_runtime(
requested_provider=requested_provider, explicit_api_key=explicit_api_key,
explicit_base_url=explicit_base_url, target_model=target_model,
)
runtime = _resolve_named_custom_runtime(requested_provider=requested_provider, explicit_api_key=explicit_api_key,
explicit_base_url=explicit_base_url, target_model=target_model)
if runtime:
runtime["requested_provider"] = requested_provider
return runtime
@@ -979,10 +920,9 @@ def _provider_rungs(provider, requested_provider, explicit_api_key, explicit_bas
returned nothing; the last rung (OpenRouter / bare-custom fallback) always yields a runtime."""
model_cfg = _get_model_config()
yield _opencode_free_runtime(provider, requested_provider, model_cfg, target_model)
yield _resolve_explicit_runtime(
provider=provider, requested_provider=requested_provider, model_cfg=model_cfg,
explicit_api_key=explicit_api_key, explicit_base_url=explicit_base_url, target_model=target_model,
)
yield _resolve_explicit_runtime(provider=provider, requested_provider=requested_provider, model_cfg=model_cfg,
explicit_api_key=explicit_api_key, explicit_base_url=explicit_base_url,
target_model=target_model)
yield _resolve_from_pool(provider, requested_provider, model_cfg, explicit_api_key, explicit_base_url, target_model)
if provider in _OAUTH_RUNTIME_PROVIDERS:
yield _resolve_oauth_runtime(provider, requested_provider, model_cfg, target_model)
+38 -57
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
@@ -63,10 +61,9 @@ def _azure_foundry_api_key(rp, explicit_api_key: str) -> str:
return api_key
def _resolve_azure_foundry_runtime(
*, requested_provider: str, model_cfg: Dict[str, Any], explicit_api_key: Optional[str] = None,
explicit_base_url: Optional[str] = None, target_model: Optional[str] = None,
) -> Dict[str, Any]:
def _resolve_azure_foundry_runtime(*, requested_provider: str, model_cfg: Dict[str, Any],
explicit_api_key: Optional[str] = None, explicit_base_url: Optional[str] = None,
target_model: Optional[str] = None) -> Dict[str, Any]:
"""Azure Foundry: ``model.base_url`` + ``model.api_mode`` (or explicit overrides), API key from
``.env``/env or a per-request Entra ID token, trailing ``/v1`` stripped for Anthropic-style
endpoints (the Anthropic SDK appends /v1/messages itself)."""
@@ -93,25 +90,20 @@ def _resolve_azure_foundry_runtime(
if cfg_api_mode == "anthropic_messages":
base_url = re.sub(r"/v1/?$", "", base_url)
if cfg_auth_mode == "entra_id":
# --api-key on the CLI while config says entra_id: honour the explicit string (escape hatch
# for one-off testing).
if explicit_api_key:
# --api-key on the CLI while config says entra_id: honour the explicit string
# (escape hatch for one-off testing).
api_key, source, auth_mode = explicit_api_key, "explicit", "api_key"
api_key, source, auth_mode, entra = explicit_api_key, "explicit", "api_key", {}
else:
api_key, source, auth_mode = _azure_entra_credentials(cfg_entra), "entra_id", "entra_id"
clean_entra = {}
configured_scope = str(cfg_entra.get("scope") or "").strip()
if auth_mode == "entra_id" and configured_scope:
clean_entra["scope"] = configured_scope
return rp._runtime(
"azure-foundry", cfg_api_mode, base_url, api_key,
auth_mode=auth_mode, entra=clean_entra, source=source, requested_provider=requested_provider,
)
return rp._runtime(
"azure-foundry", cfg_api_mode, base_url, _azure_foundry_api_key(rp, explicit_api_key),
auth_mode="api_key", source="explicit" if (explicit_api_key or explicit_base_url) else "config",
requested_provider=requested_provider,
)
scope = str(cfg_entra.get("scope") or "").strip()
api_key, source, auth_mode, entra = _azure_entra_credentials(cfg_entra), "entra_id", "entra_id", (
{"scope": scope} if scope else {}
)
return rp._runtime("azure-foundry", cfg_api_mode, base_url, api_key, auth_mode=auth_mode, entra=entra, source=source,
requested_provider=requested_provider)
return rp._runtime("azure-foundry", cfg_api_mode, base_url, _azure_foundry_api_key(rp, explicit_api_key),
auth_mode="api_key", source="explicit" if (explicit_api_key or explicit_base_url) else "config",
requested_provider=requested_provider)
# ── OpenRouter / bare custom fallback ──────────────────────────────────────────────────────
@@ -120,13 +112,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 ""
@@ -173,10 +163,8 @@ def _resolve_openrouter_runtime(
cfg_api_mode = rp._parse_api_mode(model_cfg.get("api_mode"))
# Explicit "custom" stays "custom" rather than relabeling to "openrouter".
if requested_norm != "custom":
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,
)
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:
# provider_name makes pool lookup prefer name match over base_url (fixes credential
# mix-ups when multiple custom providers share a base_url).
@@ -210,10 +198,9 @@ def _resolve_bedrock_runtime(requested_provider: str, model_cfg: Dict[str, Any],
Claude → AnthropicBedrock SDK (prompt caching, thinking budgets); others → Converse API.
AWS_BEARER_TOKEN_BEDROCK auth is unsupported by AnthropicBedrock (SigV4 only), so bearer users
go through Converse regardless of model."""
from agent.bedrock_adapter import (
bedrock_openai_base_url, has_aws_credentials, is_anthropic_bedrock_model, is_openai_bedrock_model,
resolve_aws_auth_env_var, resolve_bedrock_bearer_token, resolve_bedrock_runtime_region,
)
from agent.bedrock_adapter import (bedrock_openai_base_url, has_aws_credentials, is_anthropic_bedrock_model,
is_openai_bedrock_model, resolve_aws_auth_env_var, resolve_bedrock_bearer_token,
resolve_bedrock_runtime_region)
from hermes_cli.config import load_config # direct (not the origin delegate), as before
rp = _rp()
# Explicitly selected bedrock trusts boto3's credential chain (IMDS, ECS/Lambda roles, SSO)
@@ -235,16 +222,12 @@ def _resolve_bedrock_runtime(requested_provider: str, model_cfg: Dict[str, Any],
guardrail_config = _bedrock_guardrail_config(bedrock_cfg)
current_model = str(target_model or model_cfg.get("default") or "").strip()
has_bearer_token = bool(os.environ.get("AWS_BEARER_TOKEN_BEDROCK", "").strip())
runtime = rp._runtime(
"bedrock", "bedrock_converse", f"https://bedrock-runtime.{region}.amazonaws.com", "aws-sdk",
source=auth_source, region=region, requested_provider=requested_provider,
)
runtime = rp._runtime("bedrock", "bedrock_converse", f"https://bedrock-runtime.{region}.amazonaws.com", "aws-sdk",
source=auth_source, region=region, requested_provider=requested_provider)
if is_openai_bedrock_model(current_model):
bearer = resolve_bedrock_bearer_token()
runtime.update(
api_mode="codex_responses", base_url=bedrock_openai_base_url(region), api_key=bearer or "aws-sdk",
source="AWS_BEARER_TOKEN_BEDROCK" if bearer else auth_source, model=current_model, bedrock_openai=True,
)
runtime.update(api_mode="codex_responses", base_url=bedrock_openai_base_url(region), api_key=bearer or "aws-sdk",
source="AWS_BEARER_TOKEN_BEDROCK" if bearer else auth_source, model=current_model, bedrock_openai=True)
elif is_anthropic_bedrock_model(current_model) and not has_bearer_token:
runtime.update(api_mode="anthropic_messages", bedrock_anthropic=True)
if guardrail_config:
@@ -262,14 +245,14 @@ def _is_external_process_provider(provider: str) -> bool:
if not name:
return False
try:
from hermes_cli.auth import PROVIDER_REGISTRY
pconfig = PROVIDER_REGISTRY.get(name)
pconfig = _rp().PROVIDER_REGISTRY.get(name)
if pconfig is not None:
return pconfig.auth_type == "external_process"
except Exception:
pass
try:
from providers import get_provider_profile
profile = get_provider_profile(name)
except Exception:
return False
@@ -279,8 +262,6 @@ 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)
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,
)
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)
+65 -100
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
@@ -83,9 +80,8 @@ def _lift_extra_headers(entry: Dict[str, Any], result: Dict[str, Any]) -> None:
result["extra_headers"] = extra_headers
def _lift_common_custom_fields(
entry: Dict[str, Any], result: Dict[str, Any], *, provider_key: str, key_env: str, api_mode: Optional[str]
) -> None:
def _lift_common_custom_fields(entry: Dict[str, Any], result: Dict[str, Any], *, provider_key: str, key_env: str,
api_mode: Optional[str]) -> None:
"""Copy the optional fields shared by ``providers:`` and legacy ``custom_providers:`` entries."""
if key_env:
result["key_env"] = key_env
@@ -98,23 +94,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()
@@ -143,10 +135,8 @@ def _match_new_style_provider(requested_norm: str, providers: Dict[str, Any]) ->
if not base_url:
continue
result: Dict[str, Any] = {
"name": entry.get("name", ep_name),
"base_url": base_url.strip(),
"api_key": api_key or _clean(entry.get("api_key", "")),
"model": entry.get("default_model", ""),
"name": entry.get("name", ep_name), "base_url": base_url.strip(),
"api_key": api_key or _clean(entry.get("api_key", "")), "model": entry.get("default_model", ""),
}
# Command that PRINTS a short-lived credential; wrapped in a per-request token provider.
key_cmd = _clean(entry.get("key_cmd", ""))
@@ -177,10 +167,8 @@ def _match_legacy_custom_provider(requested_norm: str, custom_providers) -> Opti
model_name = _clean(entry.get("model", ""))
if model_name:
result["model"] = model_name
_lift_common_custom_fields(
entry, result, provider_key=provider_key, key_env=_clean(entry.get("key_env", "")),
api_mode=_rp()._parse_api_mode(entry.get("api_mode")),
)
_lift_common_custom_fields(entry, result, provider_key=provider_key, key_env=_clean(entry.get("key_env", "")),
api_mode=_rp()._parse_api_mode(entry.get("api_mode")))
return result
return None
@@ -285,18 +273,15 @@ def find_custom_provider_identity_by_model(model: str) -> Optional[str]:
return _find_custom_identity(_entry_serves_model)
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.
"""
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 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)
@@ -319,31 +304,28 @@ def canonical_custom_identity(
if not candidate_norm or candidate_norm in {"custom", "auto", "openrouter"}:
return None
# Only when it resolves to a configured entry — never invent a ``custom:<x>`` resolution
# can't honor.
# can't honor. ``candidate`` may be the entry's DISPLAY NAME, not the durable identity of a
# 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)
if entry is not None:
# ``candidate`` may be the entry's DISPLAY NAME, not the durable identity of a keyed
# ``providers:`` entry — re-resolve via its endpoint so every path returns the same
# config-key slug.
identity = find_custom_provider_identity(str(entry.get("base_url") or ""))
if identity:
return identity
return candidate_norm if candidate_norm.startswith("custom:") else f"custom:{candidate_norm}"
except Exception:
pass
return None
return None
if entry is None:
return None
try:
identity = find_custom_provider_identity(str(entry.get("base_url") or ""))
except Exception:
return None
return identity or custom_provider_slug(candidate_norm)
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
@@ -388,10 +370,8 @@ def _try_resolve_from_custom_pool(
# services; has_usable_secret's 4-char floor rejects them. Every other path
# substitutes "no-key-required" for a loopback endpoint — this was the one gap.
pool_api_key = "no-key-required"
return rp._runtime(
provider_label, api_mode_override or rp._detect_api_mode_for_url(base_url) or "chat_completions",
base_url, pool_api_key, source=f"pool:{pool_key}", credential_pool=pool,
)
return rp._runtime(provider_label, api_mode_override or rp._detect_api_mode_for_url(base_url) or "chat_completions",
base_url, pool_api_key, source=f"pool:{pool_key}", credential_pool=pool)
except Exception:
continue
return None
@@ -404,9 +384,7 @@ def _custom_provider_request_overrides(custom_provider: Dict[str, Any]) -> Dict[
return {"extra_body": dict(extra_body)}
def _apply_custom_provider_extras(
custom_provider: Dict[str, Any], target_model: Optional[str], result: Dict[str, Any]
) -> None:
def _apply_custom_provider_extras(custom_provider: Dict[str, Any], target_model: Optional[str], result: Dict[str, Any]) -> None:
"""Copy model / capabilities / max_output_tokens / extra_headers / request_overrides onto a
resolved custom runtime. An explicit ``target_model`` wins over the provider's configured
default (auxiliary slots / background-review resolve a concrete model and must not fall back to
@@ -426,11 +404,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
@@ -438,11 +414,9 @@ def _resolve_llamacpp_runtime(requested_provider: str, explicit_api_key: Optiona
except Exception: # noqa: BLE001 — resolution is best-effort
endpoint = None
if endpoint:
return rp._runtime(
"custom", "chat_completions", endpoint["base_url"],
(explicit_api_key or "").strip() or endpoint["api_key"] or "no-key-required",
source="local-runtime", requested_provider=requested_provider,
)
return rp._runtime("custom", "chat_completions", endpoint["base_url"],
(explicit_api_key or "").strip() or endpoint["api_key"] or "no-key-required", source="local-runtime",
requested_provider=requested_provider)
try:
enabled = bool((rp.load_config().get("local_runtime") or {}).get("enabled"))
except Exception: # noqa: BLE001
@@ -462,15 +436,12 @@ def _resolve_llamacpp_runtime(requested_provider: str, explicit_api_key: Optiona
def _custom_runtime(rp, base_url: str, api_key: Any, api_mode: Optional[str], **extra: Any) -> Dict[str, Any]:
"""``custom`` runtime dict with URL-detected api_mode fallback and the no-auth placeholder."""
return rp._runtime(
"custom", api_mode or rp._detect_api_mode_for_url(base_url) or "chat_completions", base_url,
api_key or "no-key-required", **extra,
)
return rp._runtime("custom", api_mode or rp._detect_api_mode_for_url(base_url) or "chat_completions", base_url,
api_key or "no-key-required", **extra)
def _resolve_direct_alias_runtime(
requested_provider: str, explicit_api_key: Optional[str], explicit_base_url: str
) -> Dict[str, Any]:
def _resolve_direct_alias_runtime(requested_provider: str, explicit_api_key: Optional[str],
explicit_base_url: str) -> Dict[str, Any]:
"""Bare ``custom`` + explicit base_url (e.g. a ``model_aliases:`` direct alias)."""
rp = _rp()
base_url = explicit_base_url.strip().rstrip("/")
@@ -501,16 +472,13 @@ def _opencode_family_for_custom(requested_provider: str, base_url: str) -> Optio
return None
def _resolve_named_custom_runtime(
*, requested_provider: str, explicit_api_key: Optional[str] = None, explicit_base_url: Optional[str] = None,
target_model: Optional[str] = None,
) -> Optional[Dict[str, Any]]:
def _resolve_named_custom_runtime(*, requested_provider: str, explicit_api_key: Optional[str] = None,
explicit_base_url: Optional[str] = None,
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:
@@ -547,15 +515,12 @@ def _resolve_named_custom_runtime(
key_cmd = _clean(custom_provider.get("key_cmd", ""))
if key_cmd and not rp.has_usable_secret(explicit_key):
from agent.command_token_source import build_command_token_provider
token_provider = build_command_token_provider(
key_cmd, str(custom_provider.get("name", requested_provider) or "custom")
)
token_provider = build_command_token_provider(key_cmd, str(custom_provider.get("name", requested_provider) or "custom"))
if token_provider is not None:
api_key = token_provider
result = _custom_runtime(
rp, base_url, api_key, custom_provider.get("api_mode"),
source=f"custom_provider:{custom_provider.get('name', requested_provider)}", requested_provider=requested_provider,
)
result = _custom_runtime(rp, base_url, api_key, custom_provider.get("api_mode"),
source=f"custom_provider:{custom_provider.get('name', requested_provider)}",
requested_provider=requested_provider)
_apply_custom_provider_extras(custom_provider, target_model, result)
# OpenCode-family custom providers (opencode-go/zen names, or opencode.ai hosts) serve models
# on different API surfaces — a static api_mode 503s for /v1/responses-only models. Re-derive