# NousResearch/hermes-agent · Model picker 50-entry cap hides provider models

## Open now

- 2 issues, 2 PRs open on GitHub.
- #42270: hermes --tui /model picker hard-caps model list at 50 entries, hiding models for providers with large catalogs (e.g. NVIDIA NIM)
- #59211: \[Bug\]: MoA configure shows only 2 NVIDIA models while normal model picker shows many NVIDIA models
- PR #26153: fix(model-picker): add minimax-cn to _MODELS_DEV_PREFERRED and include M2.7-highspeed
- PR #60764: fix(model): expand NVIDIA catalog fallback

A hard cap of 50 entries in the interactive model picker silently truncates large provider catalogs, hiding valid models. Multiple bug reports and fix attempts address raising or removing this limit.

## TL;DR

- 21 member(s): 2 open issue(s), 6 closed issue(s), 2 open PR(s), 1 merged PR(s), 10 PR(s) closed without merging.
- A fix was merged to main in PR #48611. Verify it addresses your case.
- **Recommended:** read PR #48611 (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: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.10`: 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.

## Open related issues

- #42270: hermes --tui /model picker hard-caps model list at 50 entries, hiding models for providers with large catalogs (e.g. NVIDIA NIM)
- #59211: \[Bug\]: MoA configure shows only 2 NVIDIA models while normal model picker shows many NVIDIA models

## Closed / duplicate issues

- #26146: \[Bug\]: /model slash command picker misses models visible in hermes setup model
- #35443: CLI /model picker only exposes first 50 models despite larger provider catalog
- #40601: \[Bug\]: /model TUI picker hard-caps results at 50 entries, blocking NVIDIA (and other) providers with 100+
- #48279: \[Bug\]: Kilo Gateway picker hides 286 of 336 catalog models — search returns nothing for cached models beyond position 50
- #48378: max_models=50 cap in model picker pipeline silently hides valid models (e.g. -free variants)
- #49911: NVIDIA NIM provider: max_models=50 truncation hides GLM, Kimi, and Nemotron models

## Prior & competing PRs

- **#42343: fix(cli): raise model picker max_models cap from 50 to 200**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#40765`, `#40765`, `#40747`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#42407: fix(model_switch): raise picker max_models cap from 50 to 200**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#40765`, `#42270`, `#40601`, `#40765`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#48611: fix(picker): remove max_models=50 cap in interactive model pickers**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `cli.py`, `hermes_cli/inventory.py`, `hermes_cli/model_switch.py`, `hermes_cli/web_server.py`, `tests/hermes_cli/test_inventory.py`, `tests/hermes_cli/test_model_catalog.py`, `tui_gateway/server.py`
  - fix hunks:
  - `cli.py`

    ```diff
    @@ -6930,7 +6930,7 @@ def _handle_model_switch(self, cmd_original: str):
                 try:
                     if ctx is None:
                         raise RuntimeError("inventory context unavailable")
    -                providers = build_models_payload(ctx, max_models=50)["providers"]
    +                providers = build_models_payload(ctx)["providers"]
                 except Exception:
                     providers = []
     
    ```
  - `hermes_cli/inventory.py`

    ```diff
    @@ -117,7 +117,7 @@ def build_models_payload(
         pricing: bool = False,
         capabilities: bool = False,
         force_fresh_nous_tier: bool = False,
    -    max_models: int = 50,
    +    max_models: int | None = None,
     ) -> dict:
         """Build the ``{providers, model, provider}`` shape every consumer
         needs from a single substrate call.
    ```
  - `hermes_cli/model_switch.py`

    ```diff
    @@ -1188,7 +1188,6 @@ def _warm() -> None:
                     current_model=ctx.current_model,
                     user_providers=ctx.user_providers,
                     custom_providers=ctx.custom_providers,
    -                max_models=50,
                 )
             except Exception:
                 # Best-effort warmup — never surface errors into the session.
    ```

    ```diff
    @@ -1206,7 +1205,7 @@ def list_authenticated_providers(
         custom_providers: list | None = None,
         *,
         force_fresh_nous_tier: bool = False,
    -    max_models: int = 8,
    +    max_models: int | None = None,
         current_model: str = "",
     ) -> List[dict]:
         """Detect which providers have credentials and list their curated models.
    ```

    ```diff
    @@ -1426,7 +1425,7 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:
                 if hermes_id in _MODELS_DEV_PREFERRED:
                     model_ids = _merge_with_models_dev(hermes_id, model_ids)
             total = len(model_ids)
    -        top = model_ids[:max_models]
    +        top = model_ids[:max_models] if max_models is not None else model_ids
     
             slug = hermes_id
             pinfo = _mdev_pinfo(mdev_id)
    ```

    ```diff
    @@ -1589,7 +1588,7 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:
                     if hermes_slug in _MODELS_DEV_PREFERRED:
                         model_ids = _merge_with_models_dev(hermes_slug, model_ids)
             total = len(model_ids)
    -        top = model_ids[:max_models]
    +        top = model_ids[:max_models] if max_models is not None else model_ids
     
             results.append({
                 "slug": hermes_slug,
    ```

    ```diff
    @@ -1664,7 +1663,7 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:
                 if not _cp_model_ids:
                     _cp_model_ids = curated.get(_cp.slug, [])
             _cp_total = len(_cp_model_ids)
    -        _cp_top = _cp_model_ids[:max_models]
    +        _cp_top = _cp_model_ids[:max_models] if max_models is not None else _cp_model_ids
     
             results.append({
                 "slug": _cp.slug,
    ```

    ```diff
    @@ -1813,7 +1812,7 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:
                 "name": "Custom endpoint",
                 "is_current": True,
                 "is_user_defined": True,
    -            "models": _models[:max_models] if max_models else _models,
    +            "models": _models[:max_models] if max_models is not None else _models,
                 "total_models": len(_models),
                 "source": "model-config",
                 "api_url": str(current_base_url).strip().rstrip("/"),
    ```

    ```diff
    @@ -2040,7 +2039,7 @@ def list_picker_providers(
         current_base_url: str = "",
         user_providers: dict = None,
         custom_providers: list | None = None,
    -    max_models: int = 8,
    +    max_models: int | None = None,
         current_model: str = "",
     ) -> List[dict]:
         """Interactive-picker variant of :func:`list_authenticated_providers`.
    ```

    ```diff
    @@ -2083,7 +2082,7 @@ def list_picker_providers(
                 except Exception:
                     live_ids = list(p.get("models", []))
                 p = dict(p)
    -            p["models"] = live_ids[:max_models]
    +            p["models"] = live_ids[:max_models] if max_models is not None else live_ids
                 p["total_models"] = len(live_ids)
     
             has_models = bool(p.get("models"))
    ```
  - `hermes_cli/web_server.py`

    ```diff
    @@ -3323,7 +3323,6 @@ def get_model_options(profile: Optional[str] = None):
             with _profile_scope(profile):
                 return build_models_payload(
                     load_picker_context(),
    -                max_models=50,
                     include_unconfigured=True,
                     picker_hints=True,
                     canonical_order=True,
    ```

    ```diff
    @@ -3398,7 +3397,7 @@ def get_recommended_default_model(provider: str = ""):
         try:
             from hermes_cli.inventory import build_models_payload, load_picker_context
     
    -        payload = build_models_payload(load_picker_context(), max_models=50)
    +        payload = build_models_payload(load_picker_context())
             for row in payload.get("providers", []):
                 if str(row.get("slug", "")).lower() == slug:
                     models = row.get("models") or []
    ```
  - `tests/hermes_cli/test_inventory.py`

    ```diff
    @@ -660,3 +660,31 @@ def test_two_custom_providers_with_overlap_both_survive():
         assert a_row["total_models"] == 2
         assert b_row["total_models"] == 2
     
    +
    +def test_build_models_payload_no_max_models_returns_full_list():
    +    """When max_models is not passed (None), build_models_payload must
    +    return the full model list — not truncate to the old default of 50.
    +    Regression for #48279: Kilo Gateway picker was capped at 50 of 336
    +    models, making most models undiscoverable via search."""
    +    full_models = [f"model-{i}" for i in range(100)]
    +    rows = [
    +        {
    +            "slug": "kilocode",
    +            "name": "Kilo Code",
    +            "models": full_models,
    +            "total_models": len(full_models),
    +            "is_current": False,
    +            "is_user_defined": False,
    +            "source": "built-in",
    +        },
    +    ]
    +    ctx = _empty_ctx()
    +    with _list_auth_returning(rows):
    +        # No max_models argument — should return all 100 models
    +        payload = build_models_payload(ctx)
    +
    +    kilo_row = next(r for r in payload["providers"] if r["slug"] == "kilocode")
    +    assert kilo_row["models"] == full_models
    +    assert kilo_row["total_models"] == 100
    +    assert len(kilo_row["models"]) == 100
    +
    ```
  - `tests/hermes_cli/test_model_catalog.py`

    ```diff
    @@ -423,6 +423,71 @@ def test_picker_nous_row_uses_curated_list(self, tmp_path, monkeypatch):
             assert nous_row is not None, "nous row must appear when authed"
             assert nous_row["models"] == expected
     
    +    def test_picker_max_models_cap_semantics(self, tmp_path, monkeypatch):
    +        """The cap argument has three distinct meanings on the real slicing
    +        path: ``None`` = unlimited (the cap-removal fix, #48297), ``0`` = no
    +        models (preserved for slug-only callers), an int N = first N. Guards
    +        the ``is not None`` distinction the cap-removal follow-up introduced —
    +        a ``if max_models`` (falsy) check would conflate ``0`` with unlimited.
    +        """
    +        import importlib
    +        from hermes_cli import model_catalog
    +        from hermes_cli.models import get_curated_nous_model_ids
    +        importlib.reload(model_catalog)
    +        try:
    +            from hermes_cli.model_switch import (
    +                list_authenticated_providers,
    +                list_picker_providers,
    +            )
    +
    +            active_home = Path(os.environ["HERMES_HOME"])
    +            (active_home / "auth.json").write_text(
    +                json.dumps(
    +                    {
    +                        "providers": {"nous": {"access_token": "fake"}},
    +                        "credential_pool": {},
    +                    }
    +                )
    +            )
    +            with patch.object(
    +                model_catalog, "_fetch_manifest", return_value=_valid_manifest()
    +            ), patch("hermes_cli.models.check_nous_free_tier", return_value=False), patch(
    +                "hermes_cli.models.union_with_portal_free_recommendations",
    +                side_effect=lambda ids, *a, **k: (ids, {}),
    +            ), patch(
    +                "hermes_cli.models.union_with_portal_paid_recommendations",
    +                side_effect=lambda ids, *a, **k: (ids, {}),
    +            ):
    +                expected = get_curated_nous_model_ids()
    +                full = list_picker_providers(current_provider="nous", max_models=None)
    +                one = list_picker_providers(current_provider="nous", max_models=1)
    +                # 0 is exercised on list_authenticated_providers (the slug-only
    +                # path); the picker variant drops empty-model rows entirely, so
    +                # the empty-list contract lives on the auth-providers call.
    +                zero = list_authenticated_providers(
    +                    current_provider="nous", max_models=0
    +                )
    +        finally:
    +            model_catalog.reset_cache()
    +
    +        def _nous(rows):
    +            return next((r for r in rows if r["slug"] == "nous"), None)
    +
    +        # Only meaningful when the curated list actually exceeds 1 entry.
    +        assert len(expected) > 1, "test needs a multi-model curated nous list"
    +
    +        full_row = _nous(full)
    +        assert full_row is not None and full_row["models"] == expected
    +
    +        one_row = _nous(one)
    +        assert one_row is not None and one_row["models"] == expected[:1]
    +
    +        zero_row = _nous(zero)
    +        # 0 means an empty model list — NOT unlimited. total_models still real.
    +        assert zero_row is not None
    +        assert zero_row["models"] == []
    +        assert zero_row["total_models"] == len(expected)
    +
     
     # -----------------------------------------------------------------------------
     # Drift guard — prevent the in-repo curated lists from going out of sync with
    ```
  - `tui_gateway/server.py`

    ```diff
    @@ -9280,7 +9280,6 @@ def _(rid, params: dict) -> dict:
                 canonical_order=True,
                 pricing=True,
                 capabilities=True,
    -            max_models=50,
             )
             return _ok(rid, payload)
         except Exception as e:
    ```
- **#26153: fix(model-picker): add minimax-cn to _MODELS_DEV_PREFERRED and include M2.7-highspeed**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `hermes_cli/models.py`, `tests/test_minimax_model_validation.py`
- **#26166: fix(hermes_cli): add minimax-cn to _MODELS_DEV_PREFERRED**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#26153`, `#26146`, `#26153`
- **#27914: fix(model-picker): hydrate truncated provider model lists**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#48611`, `#48297`
- **#35446: fix(cli): load full model list in picker**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#48611`, `#48297`
- **#35447: fix(model-picker): load all models in interactive pickers instead of capping at 50**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#48611`, `#48297`
- **#40747: fix(cli): raise model picker catalog limit**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `9705e7944ae46401ab9cb011ebd9fbcd5667b981`, `#40601`
- **#40765: fix(cli): raise model picker catalog limit (#40601)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#48611`, `#48297`
- **#42496: fix(cli): raise model picker cap from 50 to 200 to surface all provider models**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#48611`, `#48297`
- **#48297: fix(picker): remove max_models=50 cap in interactive model pickers**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#48611`
- **#60764: fix(model): expand NVIDIA catalog fallback**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `240afd0b7`, `4043bb4ad`
  - files: `hermes_cli/models.py`, `tests/hermes_cli/test_nvidia_catalog_fallback.py`

## Fix status

- Merged to main in PR #48611 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-18`.
- Subsystem: `apps/desktop`
