From 201f7caed8432a1afaf5bc485259d696aa16e7a5 Mon Sep 17 00:00:00 2001 From: Andy <16577466+andy825@user.noreply.gitee.com> Date: Wed, 29 Apr 2026 22:15:56 +0800 Subject: [PATCH] fix: prevent bare 'custom' slug in model.provider (#17478) When hermes model picker switches to a custom_providers entry, the slug assignment can write the literal string 'custom' to model.provider if a prior failed switch already left that value in config.yaml. Two fixes: 1. model_switch.py: filter out bare 'custom' in slug assignment, always resolve to canonical custom: form 2. providers.py: resolve_custom_provider() self-heals bare 'custom' by falling back to the first valid custom_providers entry Closes #17478 --- hermes_cli/model_switch.py | 9 ++++++++- hermes_cli/providers.py | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/hermes_cli/model_switch.py b/hermes_cli/model_switch.py index f7a6136705..4f57f9cef5 100644 --- a/hermes_cli/model_switch.py +++ b/hermes_cli/model_switch.py @@ -1503,7 +1503,14 @@ def list_authenticated_providers( current_base_url and api_url == current_base_url.strip().rstrip("/") ): - slug = current_provider or custom_provider_slug(display_name) + # Guard against bare "custom" slug left by a prior + # failed switch — always resolve to the canonical + # custom: form. (GH #17478) + slug = ( + current_provider + if current_provider and current_provider != "custom" + else custom_provider_slug(display_name) + ) else: slug = custom_provider_slug(display_name) groups[group_key] = { diff --git a/hermes_cli/providers.py b/hermes_cli/providers.py index 4909870954..f766a50ebf 100644 --- a/hermes_cli/providers.py +++ b/hermes_cli/providers.py @@ -585,6 +585,12 @@ def resolve_custom_provider( if not requested: return None + # If the stored provider is the bare string "custom" (corrupt state + # from a prior model-switch bug), fall back to the first custom + # provider entry so existing configs self-heal. (GH #17478) + bare_custom_fallback = requested == "custom" + first_valid = None + for entry in custom_providers: if not isinstance(entry, dict): continue @@ -599,6 +605,10 @@ def resolve_custom_provider( if not display_name or not api_url: continue + # Stash the first valid entry for bare-"custom" fallback + if first_valid is None: + first_valid = (display_name, api_url) + slug = custom_provider_slug(display_name) if requested not in {display_name.lower(), slug}: continue @@ -614,6 +624,21 @@ def resolve_custom_provider( source="user-config", ) + # Self-heal: bare "custom" matched nothing — return first valid entry + if bare_custom_fallback and first_valid: + dname, aurl = first_valid + slug = custom_provider_slug(dname) + return ProviderDef( + id=slug, + name=dname, + transport="openai_chat", + api_key_env_vars=(), + base_url=aurl, + is_aggregator=False, + auth_type="api_key", + source="user-config", + ) + return None