# NousResearch/hermes-agent · Fixes for bare 'custom' slug written to model.provider

## Open now

- 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.

## TL;DR

- 5 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 3 PR(s) closed without merging.
- A fix was merged to main in PR #17891. Verify it addresses your case.
- **Recommended:** read PR #17891 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `resolved` · deciding PR(s): `pr-17891`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.40`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Closed / duplicate issues

- #17478: bug:  picker writes literal 'custom' to model.provider instead of actual provider name

## Prior & competing PRs

- **#17580: fix(cli): reject literal 'custom' as provider slug in model picker**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#17481`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#17671: fix(model-switch): skip "custom"/"local" sentinel when assigning picker slug (#17478)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#17481`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#17891: fix(model_switch): prevent bare 'custom' slug in model.provider (#17478)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/model_switch.py`, `hermes_cli/providers.py`, `scripts/release.py`, `tests/hermes_cli/test_model_switch_custom_providers.py`
  - fix hunks:
  - `hermes_cli/model_switch.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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=[
    ```

    ```diff
    @@ -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."""
    ```
- **#17481: fix: prevent bare 'custom' slug in model.provider (#17478)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `201f7caed8`, `61fec7689d`, `3fc4c63d38`, `#17891`

## Fix status

- Merged to main in PR #17891 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-04-30`.
