Merge branch 'simp/r3-23-g3' into simp/integration3
This commit is contained in:
+49
-77
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user