mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-11 08:42:11 +00:00
fix(cron): resolve per-job provider "custom" to providers.custom instead of codex
A cron job stored with `provider: "custom"` and a matching `providers.custom` entry in config failed at execution with `auth_unavailable: providers=codex`. Two layers conspired: - `_get_named_custom_provider` returned None for bare "custom" *before* scanning config, so a literal `providers.custom` entry was never matched and resolution fell through to the global default (codex). Now it scans config for an entry literally named "custom"; with none it still returns None, preserving the legacy model.base_url trust path. - `_resolve_model_override` blindly stripped bare "custom" at job creation and pinned `model.provider` (e.g. codex). It now keeps "custom" when a configured custom endpoint resolves, pinning the main provider only when it doesn't.
This commit is contained in:
parent
1e7316ced2
commit
acd4f34e65
2 changed files with 44 additions and 10 deletions
|
|
@ -491,15 +491,27 @@ def _lift_max_output_tokens(entry: Dict[str, Any], result: Dict[str, Any]) -> No
|
|||
|
||||
def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, Any]]:
|
||||
requested_norm = _normalize_custom_provider_name(requested_provider or "")
|
||||
if not requested_norm or requested_norm == "custom":
|
||||
if not requested_norm:
|
||||
return None
|
||||
|
||||
# Bare "custom" is normally an incomplete spec — the canonical form is
|
||||
# "custom:<name>" — and is otherwise owned by the model.base_url "bare
|
||||
# custom" trust path. BUT a user may literally name a ``providers:`` (or
|
||||
# legacy ``custom_providers:``) entry "custom" (e.g. ``providers.custom``
|
||||
# pointing at cliproxy). We used to return None here *before* scanning
|
||||
# config, so such an entry was never matched and resolution fell through to
|
||||
# the global default (Codex) — the cause of cron jobs with
|
||||
# ``provider: "custom"`` failing with ``auth_unavailable: providers=codex``.
|
||||
# Fall through to the config scan instead; if no entry is literally named
|
||||
# "custom" it still returns None at the end, preserving the trust path.
|
||||
|
||||
# Raw names should only map to custom providers when they are not already
|
||||
# valid built-in providers or aliases. Explicit menu keys like
|
||||
# ``custom:local`` always target the saved custom provider.
|
||||
# ``custom:local`` always target the saved custom provider. Bare "custom"
|
||||
# is exempt from the shadow check — it is not a built-in to defer to.
|
||||
if requested_norm == "auto":
|
||||
return None
|
||||
if not requested_norm.startswith("custom:"):
|
||||
if requested_norm != "custom" and not requested_norm.startswith("custom:"):
|
||||
try:
|
||||
canonical = auth_mod.resolve_provider(requested_norm)
|
||||
except AuthError:
|
||||
|
|
@ -634,6 +646,20 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An
|
|||
return None
|
||||
|
||||
|
||||
def has_named_custom_provider(requested_provider: str) -> bool:
|
||||
"""Return True when config defines a custom provider matching the request.
|
||||
|
||||
Thin public wrapper around :func:`_get_named_custom_provider` so other
|
||||
modules (e.g. the cronjob tool) can decide whether a provider name will
|
||||
actually resolve to a configured ``providers:`` / ``custom_providers:``
|
||||
entry — without reaching into a private helper or duplicating the scan.
|
||||
"""
|
||||
try:
|
||||
return _get_named_custom_provider(requested_provider) is not None
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _custom_provider_request_overrides(custom_provider: Dict[str, Any]) -> Dict[str, Any]:
|
||||
extra_body = custom_provider.get("extra_body")
|
||||
if not isinstance(extra_body, dict) or not extra_body:
|
||||
|
|
|
|||
|
|
@ -326,15 +326,23 @@ def _resolve_model_override(model_obj: Optional[Dict[str, Any]]) -> tuple:
|
|||
return (None, None)
|
||||
model_name = (model_obj.get("model") or "").strip() or None
|
||||
provider_name = (model_obj.get("provider") or "").strip() or None
|
||||
# Bare "custom" is an incomplete spec — the canonical form is
|
||||
# "custom:<name>" matching a custom_providers entry. LLMs frequently
|
||||
# Bare "custom" is usually an incomplete spec — the canonical form is
|
||||
# "custom:<name>" matching a custom_providers entry, and LLMs frequently
|
||||
# supply the bare type because the schema does not advertise the
|
||||
# ":<name>" suffix, which used to bypass the pinning path below and
|
||||
# leave the job stored with an unresolvable "custom" provider. Treat
|
||||
# the bare value as "no provider supplied" so the current main
|
||||
# provider gets pinned instead.
|
||||
# ":<name>" suffix. It is only a problem when it can't resolve at runtime:
|
||||
# a user may literally name a ``providers.custom`` (or custom_providers
|
||||
# "custom") entry, in which case the job should keep ``provider="custom"``
|
||||
# and run against that endpoint. Only when no such entry exists do we treat
|
||||
# the bare value as "no provider supplied" and pin the current main
|
||||
# provider below — otherwise pinning to ``model.provider`` (e.g. codex)
|
||||
# silently hijacks a job that meant to use the configured custom endpoint.
|
||||
if provider_name == "custom":
|
||||
provider_name = None
|
||||
try:
|
||||
from hermes_cli.runtime_provider import has_named_custom_provider
|
||||
if not has_named_custom_provider("custom"):
|
||||
provider_name = None
|
||||
except Exception:
|
||||
provider_name = None
|
||||
if model_name and not provider_name:
|
||||
# Pin to the current main provider so the job is stable
|
||||
try:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue