# NousResearch/hermes-agent · CLI model picker slow with custom provider discovery

## Open now

- Nothing open on GitHub.

Reports and fixes addressing slow performance in the CLI /model picker when custom providers have discover_models enabled. The work involves parallelizing or skipping provider probing to reduce latency.

## TL;DR

- 3 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 1 PR(s) closed without merging.
- A fix was merged to main in PR #65652. Verify it addresses your case.
- **Recommended:** read PR #65652 (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-65652`. 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.67`: 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

- #65650: /model picker slow (~5s) when custom providers have discover_models enabled (CLI)

## Prior & competing PRs

- **#65652: fix: speed up CLI /model picker by skipping non-current custom provider probing**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `cli.py`, `hermes_cli/model_switch.py`, `tests/hermes_cli/test_inventory.py`, `tests/hermes_cli/test_model_switch_custom_providers.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -8087,7 +8087,11 @@ def _handle_model_switch(self, cmd_original: str):
                 try:
                     if ctx is None:
                         raise RuntimeError("inventory context unavailable")
    -                providers = build_models_payload(ctx)["providers"]
    +                providers = build_models_payload(
    +                    ctx,
    +                    probe_custom_providers=force_refresh,
    +                    probe_current_custom_provider=not force_refresh,
    +                )["providers"]
                 except Exception:
                     providers = []
     
    ```
  - `hermes_cli/model_switch.py`

    ```diff
    @@ -100,6 +100,52 @@ def _add(candidate: Any) -> None:
         return ids
     
     
    +def _save_discovered_models_to_config(
    +    api_url: str, model_ids: list[str]
    +) -> None:
    +    """Persist discovered models into ``custom_providers`` in config.yaml.
    +
    +    Called after a successful ``/v1/models`` probe so that the next read
    +    with ``discover_models: false`` uses the cached list instead of a stale
    +    or minimal manually-configured subset.
    +
    +    Matches entries by ``base_url`` (trailing-slash-normalised).  A failed
    +    config write is swallowed — the picker still shows the live models for
    +    this session.
    +    """
    +    if not api_url or not model_ids:
    +        return
    +    try:
    +        from hermes_cli.config import load_config, save_config
    +
    +        cfg = load_config()
    +        providers = cfg.get("custom_providers") or []
    +        if not isinstance(providers, list):
    +            return
    +
    +        norm_url = api_url.strip().rstrip("/").lower()
    +        changed = False
    +        for entry in providers:
    +            if not isinstance(entry, dict):
    +                continue
    +            entry_url = (entry.get("base_url", "") or entry.get("url", "") or "").strip()
    +            if entry_url.rstrip("/").lower() != norm_url:
    +                continue
    +            existing = entry.get("models")
    +            # Only update when models are stale — avoids unnecessary
    +            # config writes on every picker open.
    +            if isinstance(existing, list) and existing == model_ids:
    +                continue
    +            entry["models"] = model_ids
    +            changed = True
    +
    +        if changed:
    +            cfg["custom_providers"] = providers
    +            save_config(cfg)
    +    except Exception:
    +        pass
    +
    +
     def _bare_custom_provider_def(current_base_url: str) -> Optional[ProviderDef]:
         """ProviderDef for a direct ``model.provider: custom`` endpoint."""
         base_url = str(current_base_url or "").strip()
    ```

    ```diff
    @@ -2376,6 +2422,12 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:
                         if live_models:
                             grp["models"] = live_models
                             grp["total_models"] = len(live_models)
    +                        # Auto-save discovered models back to config so
    +                        # ``discover_models: false`` has a populated cache
    +                        # on the next read.  A failed save is non-fatal.
    +                        _save_discovered_models_to_config(
    +                            api_url, live_models
    +                        )
                     except Exception:
                         pass
                 results.append({
    ```
  - `tests/hermes_cli/test_inventory.py`

    ```diff
    @@ -276,6 +276,43 @@ def test_build_models_payload_can_probe_only_current_custom_provider():
         assert mock_list.call_args.kwargs["probe_current_custom_provider"] is True
     
     
    +def test_cli_model_picker_forwards_force_refresh_to_probe_flags():
    +    """CLI /model picker must pass force_refresh to probe flags (#65652, #65650).
    +
    +    Normal open (/model bare) skips non-current probes; /model --refresh probes
    +    all custom providers to freshen their model lists.
    +    """
    +    ctx = _empty_ctx()
    +
    +    # Normal open — skip non-current probes
    +    force_refresh = False
    +    with patch(
    +        "hermes_cli.model_switch.list_authenticated_providers",
    +        return_value=[],
    +    ) as mock_list:
    +        build_models_payload(
    +            ctx,
    +            probe_custom_providers=force_refresh,
    +            probe_current_custom_provider=not force_refresh,
    +        )
    +    assert mock_list.call_args.kwargs["probe_custom_providers"] is False
    +    assert mock_list.call_args.kwargs["probe_current_custom_provider"] is True
    +
    +    # Refresh open — probe everything
    +    force_refresh = True
    +    with patch(
    +        "hermes_cli.model_switch.list_authenticated_providers",
    +        return_value=[],
    +    ) as mock_list:
    +        build_models_payload(
    +            ctx,
    +            probe_custom_providers=force_refresh,
    +            probe_current_custom_provider=not force_refresh,
    +        )
    +    assert mock_list.call_args.kwargs["probe_custom_providers"] is True
    +    assert mock_list.call_args.kwargs["probe_current_custom_provider"] is False
    +
    +
     def test_list_authenticated_providers_force_fresh_is_keyword_only():
         """``force_fresh_nous_tier`` must be keyword-only on the public listing API.
     
    ```
  - `tests/hermes_cli/test_model_switch_custom_providers.py`

    ```diff
    @@ -1326,3 +1326,156 @@ def test_resolve_custom_provider_bare_custom_self_heal_passes_key_env():
     
         assert resolved is not None
         assert resolved.api_key_env_vars == ("XIAOMI_MIMO_API_KEY",)
    +
    +
    +def test_discovered_models_auto_saved_to_cache(monkeypatch):
    +    """Discovered models are persisted to config so ``discover_models: false``
    +    has a populated cache on the next read (#65652).
    +
    +    When a successful probe returns live models, ``_save_discovered_models_to_config``
    +    must be called with the provider's base_url and the discovered model list.
    +    """
    +    monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {})
    +    monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {})
    +
    +    save_calls = []
    +
    +    def fake_fetch_api_models(api_key, base_url, **kwargs):
    +        return ["discovered-a", "discovered-b", "discovered-c"]
    +
    +    monkeypatch.setattr("hermes_cli.models.fetch_api_models", fake_fetch_api_models)
    +    monkeypatch.setattr(
    +        "hermes_cli.model_switch._save_discovered_models_to_config",
    +        lambda api_url, model_ids: save_calls.append((api_url, model_ids)),
    +    )
    +
    +    custom_providers = [
    +        {
    +            "name": "my-gateway",
    +            "api_key": "***",
    +            "base_url": "https://gateway.example.com/v1",
    +            "discover_models": True,
    +            "model": "only-model",
    +            "models": {"only-model": {"context_length": 128000}},
    +        }
    +    ]
    +
    +    providers = list_authenticated_providers(
    +        current_provider="my-gateway",
    +        current_base_url="https://gateway.example.com/v1",
    +        custom_providers=custom_providers,
    +        max_models=50,
    +        probe_custom_providers=True,
    +    )
    +
    +    assert len(save_calls) == 1, (
    +        "_save_discovered_models_to_config must be called after a successful probe"
    +    )
    +    assert save_calls[0][0] == "https://gateway.example.com/v1"
    +    assert save_calls[0][1] == ["discovered-a", "discovered-b", "discovered-c"]
    +
    +    gateway_prov = next(
    +        (p for p in providers if p.get("api_url") == "https://gateway.example.com/v1"),
    +        None,
    +    )
    +    assert gateway_prov is not None
    +    assert gateway_prov["models"] == ["discovered-a", "discovered-b", "discovered-c"]
    +
    +
    +def test_discovered_models_not_saved_on_empty_probe(monkeypatch):
    +    """When a probe returns an empty list, no auto-save must happen (#65652)."""
    +    monkeypatch.setattr("agent.models_dev.fetch_models_dev", lambda: {})
    +    monkeypatch.setattr("hermes_cli.providers.HERMES_OVERLAYS", {})
    +
    +    save_calls = []
    +
    +    def fake_fetch_api_models(api_key, base_url, **kwargs):
    +        return []
    +
    +    monkeypatch.setattr("hermes_cli.models.fetch_api_models", fake_fetch_api_models)
    +    monkeypatch.setattr(
    +        "hermes_cli.model_switch._save_discovered_models_to_config",
    +        lambda api_url, model_ids: save_calls.append((api_url, model_ids)),
    +    )
    +
    +    custom_providers = [
    +        {
    +            "name": "my-gateway",
    +            "api_key": "***",
    +            "base_url": "https://gateway.example.com/v1",
    +            "discover_models": True,
    +            "model": "only-model",
    +        }
    +    ]
    +
    +    list_authenticated_providers(
    +        current_provider="my-gateway",
    +        current_base_url="https://gateway.example.com/v1",
    +        custom_providers=custom_providers,
    +        max_models=50,
    +        probe_custom_providers=True,
    +    )
    +
    +    assert save_calls == [], "Empty probe must not trigger a save"
    +
    +
    +def test_save_discovered_models_skips_unchanged(monkeypatch):
    +    """``_save_discovered_models_to_config`` must not write config when the
    +    model list hasn't changed (#65652)."""
    +    from hermes_cli.model_switch import _save_discovered_models_to_config
    +
    +    save_calls = []
    +
    +    def fake_save(config):
    +        save_calls.append(dict(config))
    +
    +    monkeypatch.setattr("hermes_cli.config.save_config", fake_save)
    +    monkeypatch.setattr(
    +        "hermes_cli.config.load_config",
    +        lambda: {
    +            "custom_providers": [
    +                {
    +                    "name": "my-gateway",
    +                    "base_url": "https://gateway.example.com/v1",
    +                    "models": ["model-a", "model-b"],
    +                }
    +            ]
    +        },
    +    )
    +
    +    # Same list — no write
    +    _save_discovered_models_to_config(
    +        "https://gateway.example.com/v1",
    +        ["model-a", "model-b"],
    +    )
    +    assert save_calls == [], "Unchanged models must not trigger config write"
    +
    +    # Changed list — write
    +    _save_discovered_models_to_config(
    +        "https://gateway.example.com/v1",
    +        ["model-a", "model-b", "model-c"],
    +    )
    +    assert len(save_calls) == 1, "Changed models must trigger config write"
    +    updated = save_calls[0]["custom_providers"][0]
    +    assert updated["models"] == ["model-a", "model-b", "model-c"]
    +
    +
    +def test_save_discovered_models_noop_on_empty_args(monkeypatch):
    +    """``_save_discovered_models_to_config`` is a no-op when api_url or
    +    model_ids are blank (#65652)."""
    +    from hermes_cli.model_switch import _save_discovered_models_to_config
    +
    +    load_calls = 0
    +
    +    def fake_load():
    +        nonlocal load_calls
    +        load_calls += 1
    +        return {"custom_providers": []}
    +
    +    monkeypatch.setattr("hermes_cli.config.load_config", fake_load)
    +
    +    _save_discovered_models_to_config("", ["a"])
    +    _save_discovered_models_to_config("https://x.com", [])
    +    _save_discovered_models_to_config("", [])
    +
    +    assert load_calls == 0, "load_config must not be called for empty args"
    ```
- **#65766: perf(cli): parallelize custom provider model discovery in /model picker**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`

## Fix status

- Merged to main in PR #65652 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-19`.
- Subsystem: `hermes_cli`
