Fixes for bare 'custom' slug written to model.provider
NousResearch/hermes-agent · cluster 17478
Data as of
Nothing open on GitHub. Fix merged in #17891. Check it covers your case.
The short answer
- Known?
- Yes. 5 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #17891. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #17891. That is separate from the fix event. - Still open
- Nothing open on GitHub.
Multiple fixes and a bug report addressing an issue where the model picker writes the literal string 'custom' as the provider slug instead of the actual provider name. Changes span CLI, model-switch, and related resolution logic.
Open issues0
Open PRs0
Fixes on record
-
#17891 fix(model_switch): prevent bare 'custom' slug in model.provider (#17478)
files:
hermes_cli/model_switch.py,hermes_cli/providers.py,scripts/release.py,tests/hermes_cli/test_model_switch_custom_providers.pyShow the diff
-
hermes_cli/model_switch.py@@ -1501,7 +1501,14 @@ def _record_builtin_endpoint(slug: str) -> None: 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:<name> 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] = { -
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 -
scripts/release.py@@ -349,6 +349,7 @@ "[redacted:email]": "houziershi", "[redacted:email]": "PStarH", "[redacted:email]": "Sanjays2402", + "[redacted:email]": "Andy283", "[redacted:email]": "bingo906", "[redacted:email]": "AaronWong1999", "[redacted:email]": "DeployFaith", -
tests/hermes_cli/test_model_switch_custom_providers.py@@ -296,12 +296,13 @@ def test_list_authenticated_providers_groups_same_endpoint(monkeypatch): def test_list_authenticated_providers_current_endpoint_uses_current_slug(monkeypatch): """When current_base_url matches the grouped endpoint, the slug must equal current_provider so picker selection routes through the live - credential pipeline.""" + credential pipeline — provided current_provider is a real slug, not + the corrupt bare "custom" (see #17478).""" monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {}) monkeypatch.setattr(providers_mod, "HERMES_OVERLAYS", {}) providers = list_authenticated_providers( - current_provider="custom", + current_provider="custom:ollama", current_base_url="http://localhost:11434/v1", user_providers={}, custom_providers=[ @@ -314,10 +315,36 @@ def test_list_authenticated_providers_current_endpoint_uses_current_slug(monkeyp matches = [p for p in providers if p.get("is_user_defined")] assert len(matches) == 1 group = matches[0] - assert group["slug"] == "custom" + assert group["slug"] == "custom:ollama" assert group["is_current"] is True +def test_list_authenticated_providers_bare_custom_slug_recovers(monkeypatch): + """Regression for #17478: when a prior failed switch left the bare + literal "custom" in model.provider, the picker must NOT propagate + that broken slug. It must fall back to the canonical + ``custom:<name>`` form so the picker stays usable.""" + monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {}) + monkeypatch.setattr(providers_mod, "HERMES_OVERLAYS", {}) + + providers = list_authenticated_providers( + current_provider="custom", + current_base_url="http://localhost:11434/v1", + user_providers={}, + custom_providers=[ + {"name": "Ollama — GLM 5.1", "base_url": "http://localhost:11434/v1", + "api_key": "ollama", "model": "glm-5.1"}, + ], + max_models=50, + ) + + matches = [p for p in providers if p.get("is_user_defined")] + assert len(matches) == 1 + group = matches[0] + # Canonical slug, NOT the bare "custom" that caused #17478 + assert group["slug"] == "custom:ollama" + + def test_list_authenticated_providers_distinct_endpoints_stay_separate(monkeypatch): """Entries with different base_urls must produce separate picker rows even if some display names happen to be similar."""
-
3 PRs closed without merging
From the close comments: superseded by another PR 2 · merged via another change 1
-
#17481 fix: prevent bare 'custom' slug in model.provider (#17478) · merged via another change →
201f7ca,61fec76,3fc4c63,#17891 -
#17580 fix(cli): reject literal 'custom' as provider slug in model picker · superseded by another PR →
#17481 -
#17671 fix(model-switch): skip "custom"/"local" sentinel when assigning picker slug (#17478) · superseded by another PR →
#17481
Closed issues · 1
- Closed#17478bug: picker writes literal 'custom' to model.provider instead of actual provider name
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.40: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Merged PRs
- #17891
- Closed issues
- 1
- Attempts
- 3 PRs closed without merging
- Fix link checked
- 30 Apr 2026