From 987dd45fff7a2b9b3b5873f57f4d63b4886ffe1f Mon Sep 17 00:00:00 2001 From: Scott Scheferman Date: Sun, 26 Apr 2026 19:20:21 -0500 Subject: [PATCH 1/3] fix: suppress built-in openai provider when custom api.openai.com entry exists When list_authenticated_providers() processes section 1, the native `openai` provider (8 models from _PROVIDER_MODELS) now checks if a custom provider already claims the same base_url (https://api.openai.com/v1). If so, the built-in entry is skipped, preventing duplicate OpenAI buttons in the /model picker. Fixes: user-defined OpenAI (18 models) and built-in openai (8 models) both pointing at api.openai.com/v1 were appearing as separate buttons. --- hermes_cli/model_switch.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index 4f57f9cef54d..7a12bbfd386a 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -1094,6 +1094,22 @@ def _record_builtin_endpoint(slug: str) -> None: curated["lmstudio"] = live # --- 1. Check Hermes-mapped providers --- + # Pre-scan custom_providers for base_urls so we can suppress built-in + # providers that would duplicate a user-defined endpoint (e.g. OpenAI + # native vs. user-defined OpenAI pointing at api.openai.com/v1). + _custom_base_urls: set[str] = set() + if custom_providers and isinstance(custom_providers, list): + for entry in custom_providers: + if isinstance(entry, dict): + bu = (entry.get("base_url") or "").strip().rstrip("/") + if bu: + _custom_base_urls.add(bu.lower()) + + def _custom_provider_has_base_url(base_url: str) -> bool: + """Return True if a custom provider already claims this base_url.""" + target = base_url.strip().rstrip("/").lower() + return target in _custom_base_urls + for hermes_id, mdev_id in PROVIDER_TO_MODELS_DEV.items(): # Skip aliases that map to the same models.dev provider (e.g. # kimi-coding and kimi-coding-cn both → kimi-for-coding). @@ -1146,6 +1162,16 @@ def _record_builtin_endpoint(slug: str) -> None: pinfo = _mdev_pinfo(mdev_id) display_name = pinfo.name if pinfo else mdev_id + # Suppress built-in `openai` when a custom provider already claims + # the same base_url (e.g. api.openai.com/v1). The custom provider + # typically has a richer model list and is the intended entry. + if slug == "openai" and _custom_provider_has_base_url( + "https://api.openai.com/v1" + ): + seen_slugs.add(slug.lower()) + seen_mdev_ids.add(mdev_id) + continue + results.append({ "slug": slug, "name": display_name, From cfd462ec88dd828a47782e6ca3f985b4b0e99b8e Mon Sep 17 00:00:00 2001 From: Scott Scheferman Date: Sun, 26 Apr 2026 19:51:32 -0500 Subject: [PATCH 2/3] fix: resolve env-backed custom provider api keys --- hermes_cli/runtime_provider.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/hermes_cli/runtime_provider.py b/hermes_cli/runtime_provider.py index 3afd67e1cc60..095d8c474222 100644 --- a/hermes_cli/runtime_provider.py +++ b/hermes_cli/runtime_provider.py @@ -521,10 +521,18 @@ def _resolve_named_custom_runtime( pool_result["model"] = model_name return pool_result + configured_api_key = str(custom_provider.get("api_key", "") or "").strip() + configured_key_env = str(custom_provider.get("key_env", "") or "").strip() + configured_api_key_env = "" + if configured_api_key.startswith("env:"): + configured_api_key_env = configured_api_key.split(":", 1)[1].strip() + configured_api_key = "" + api_key_candidates = [ (explicit_api_key or "").strip(), - str(custom_provider.get("api_key", "") or "").strip(), - os.getenv(str(custom_provider.get("key_env", "") or "").strip(), "").strip(), + configured_api_key, + os.getenv(configured_key_env, "").strip(), + os.getenv(configured_api_key_env, "").strip(), os.getenv("OPENAI_API_KEY", "").strip(), os.getenv("OPENROUTER_API_KEY", "").strip(), ] From 556bebdb16a56a6c8a0bc9d49dc81bc92b9c2daa Mon Sep 17 00:00:00 2001 From: shagghiesuperstar Date: Thu, 30 Apr 2026 14:34:31 -0500 Subject: [PATCH 3/3] fix(agent): suppress intermediate retry status messages in chat thread When a streaming request to the LLM provider fails mid-stream (e.g. ReadTimeout, ConnectionError, RemoteProtocolError), Hermes retries with a fresh connection. Each retry emits a status message that accumulates permanently in the chat thread via _emit_status(). For self-hosted providers with frequent transient errors (backend restarts, rate limits, OOM recovery), these warnings accumulate and don't reflect the final outcome. Fix: Replace _emit_status() with logger.info() for intermediate retry messages. Only the final outcome (success or failure) is emitted to the user. Intermediate attempts go to logger.info and stay in logs. See: https://github.com/NousResearch/hermes-agent/issues/5151 --- run_agent.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/run_agent.py b/run_agent.py index 0f6755539dba..e9e0304462d2 100644 --- a/run_agent.py +++ b/run_agent.py @@ -7100,10 +7100,17 @@ def _call(): result["partial_tool_names"] = [] deltas_were_sent["yes"] = False first_delta_fired["done"] = False - self._emit_status( - f"⚠️ Connection dropped mid tool-call " - f"({type(e).__name__}). Reconnecting… " - f"(attempt {_stream_attempt + 2}/{_max_stream_retries + 1})" + # Intermediate retry messages are logged (not emitted) + # to avoid cluttering the chat thread with transient + # warnings that remain even after successful retries. + # See: https://github.com/NousResearch/hermes-agent/issues/5151 + logger.info( + "⚠️ Connection dropped mid tool-call " + "(%s). Reconnecting… " + "(attempt %s/%s)", + type(e).__name__, + _stream_attempt + 2, + _max_stream_retries + 1, ) self._touch_activity( f"stream retry {_stream_attempt + 2}/{_max_stream_retries + 1} " @@ -7166,10 +7173,17 @@ def _call(): type(e).__name__, e, ) - self._emit_status( - f"⚠️ Connection to provider dropped " - f"({type(e).__name__}). Reconnecting… " - f"(attempt {_stream_attempt + 2}/{_max_stream_retries + 1})" + # Intermediate retry messages are logged (not emitted) + # to avoid cluttering the chat thread with transient + # warnings that remain even after successful retries. + # See: https://github.com/NousResearch/hermes-agent/issues/5151 + logger.info( + "⚠️ Connection to provider dropped " + "(%s). Reconnecting… " + "(attempt %s/%s)", + type(e).__name__, + _stream_attempt + 2, + _max_stream_retries + 1, ) self._touch_activity( f"stream retry {_stream_attempt + 2}/{_max_stream_retries + 1} "