{"repo":"NousResearch/hermes-agent","public_id":"35443","display_label":"Model picker 50-entry cap hides models from large provider catalogs","summary":"Most members describe the interactive /model picker limiting provider model lists to 50 entries (max_models=50), which hides models for providers with large catalogs such as NVIDIA NIM and Kilo Gateway. The reports and pull requests propose removing the cap, raising it to 200, or loading the full list.","subsystem":"apps/desktop","counts":{"members":16,"open_issues":1,"open_issues_distinct":1,"open_prs":0,"closed_issues":5,"merged_prs":1,"attempt_prs":9},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":48611,"merged_at":"2026-06-18T20:47:32+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-18T20:47:32+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.14733333333333334},"fix_edges":[{"issue_number":35443,"pr_number":48611,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-18T20:47:32+00:00"},{"issue_number":40601,"pr_number":48611,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-18T20:47:32+00:00"},{"issue_number":42270,"pr_number":48611,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-18T20:47:32+00:00"},{"issue_number":48279,"pr_number":48611,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-18T20:47:32+00:00"}],"members":[{"kind":"issue","number":35443,"title":"CLI /model picker only exposes first 50 models despite larger provider catalog","url":null,"state":"closed"},{"kind":"issue","number":40601,"title":"[Bug]: /model TUI picker hard-caps results at 50 entries, blocking NVIDIA (and other) providers with 100+","url":null,"state":"closed"},{"kind":"issue","number":42270,"title":"hermes --tui /model picker hard-caps model list at 50 entries, hiding models for providers with large catalogs (e.g. NVIDIA NIM)","url":null,"state":"open"},{"kind":"issue","number":48279,"title":"[Bug]: Kilo Gateway picker hides 286 of 336 catalog models — search returns nothing for cached models beyond position 50","url":null,"state":"closed"},{"kind":"issue","number":48378,"title":"max_models=50 cap in model picker pipeline silently hides valid models (e.g. -free variants)","url":null,"state":"closed"},{"kind":"issue","number":49911,"title":"NVIDIA NIM provider: max_models=50 truncation hides GLM, Kimi, and Nemotron models","url":null,"state":"closed"},{"kind":"pr","number":27914,"title":"fix(model-picker): hydrate truncated provider model lists","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#48611","number":48611},{"kind":"pr","value":"#48297","number":48297}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":35446,"title":"fix(cli): load full model list in picker","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#48611","number":48611},{"kind":"pr","value":"#48297","number":48297}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":35447,"title":"fix(model-picker): load all models in interactive pickers instead of capping at 50","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#48611","number":48611},{"kind":"pr","value":"#48297","number":48297}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":40747,"title":"fix(cli): raise model picker catalog limit","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"9705e7944ae46401ab9cb011ebd9fbcd5667b981","number":0},{"kind":"pr","value":"#40601","number":40601}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":40765,"title":"fix(cli): raise model picker catalog limit (#40601)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#48611","number":48611},{"kind":"pr","value":"#48297","number":48297}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":42343,"title":"fix(cli): raise model picker max_models cap from 50 to 200","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#40765","number":40765},{"kind":"pr","value":"#40747","number":40747}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":42407,"title":"fix(model_switch): raise picker max_models cap from 50 to 200","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#40765","number":40765},{"kind":"pr","value":"#42270","number":42270},{"kind":"pr","value":"#40601","number":40601}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":42496,"title":"fix(cli): raise model picker cap from 50 to 200 to surface all provider models","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#48611","number":48611},{"kind":"pr","value":"#48297","number":48297}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":48297,"title":"fix(picker): remove max_models=50 cap in interactive model pickers","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#48611","number":48611}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":48611,"title":"fix(picker): remove max_models=50 cap in interactive model pickers","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["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":[{"file":"cli.py","hunks":["@@ -6930,7 +6930,7 @@ def _handle_model_switch(self, cmd_original: str):\n             try:\n                 if ctx is None:\n                     raise RuntimeError(\"inventory context unavailable\")\n-                providers = build_models_payload(ctx, max_models=50)[\"providers\"]\n+                providers = build_models_payload(ctx)[\"providers\"]\n             except Exception:\n                 providers = []\n "]},{"file":"hermes_cli/inventory.py","hunks":["@@ -117,7 +117,7 @@ def build_models_payload(\n     pricing: bool = False,\n     capabilities: bool = False,\n     force_fresh_nous_tier: bool = False,\n-    max_models: int = 50,\n+    max_models: int | None = None,\n ) -> dict:\n     \"\"\"Build the ``{providers, model, provider}`` shape every consumer\n     needs from a single substrate call."]},{"file":"hermes_cli/model_switch.py","hunks":["@@ -1188,7 +1188,6 @@ def _warm() -> None:\n                 current_model=ctx.current_model,\n                 user_providers=ctx.user_providers,\n                 custom_providers=ctx.custom_providers,\n-                max_models=50,\n             )\n         except Exception:\n             # Best-effort warmup — never surface errors into the session.","@@ -1206,7 +1205,7 @@ def list_authenticated_providers(\n     custom_providers: list | None = None,\n     *,\n     force_fresh_nous_tier: bool = False,\n-    max_models: int = 8,\n+    max_models: int | None = None,\n     current_model: str = \"\",\n ) -> List[dict]:\n     \"\"\"Detect which providers have credentials and list their curated models.","@@ -1426,7 +1425,7 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:\n             if hermes_id in _MODELS_DEV_PREFERRED:\n                 model_ids = _merge_with_models_dev(hermes_id, model_ids)\n         total = len(model_ids)\n-        top = model_ids[:max_models]\n+        top = model_ids[:max_models] if max_models is not None else model_ids\n \n         slug = hermes_id\n         pinfo = _mdev_pinfo(mdev_id)","@@ -1589,7 +1588,7 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:\n                 if hermes_slug in _MODELS_DEV_PREFERRED:\n                     model_ids = _merge_with_models_dev(hermes_slug, model_ids)\n         total = len(model_ids)\n-        top = model_ids[:max_models]\n+        top = model_ids[:max_models] if max_models is not None else model_ids\n \n         results.append({\n             \"slug\": hermes_slug,","@@ -1664,7 +1663,7 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:\n             if not _cp_model_ids:\n                 _cp_model_ids = curated.get(_cp.slug, [])\n         _cp_total = len(_cp_model_ids)\n-        _cp_top = _cp_model_ids[:max_models]\n+        _cp_top = _cp_model_ids[:max_models] if max_models is not None else _cp_model_ids\n \n         results.append({\n             \"slug\": _cp.slug,","@@ -1813,7 +1812,7 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:\n             \"name\": \"Custom endpoint\",\n             \"is_current\": True,\n             \"is_user_defined\": True,\n-            \"models\": _models[:max_models] if max_models else _models,\n+            \"models\": _models[:max_models] if max_models is not None else _models,\n             \"total_models\": len(_models),\n             \"source\": \"model-config\",\n             \"api_url\": str(current_base_url).strip().rstrip(\"/\"),","@@ -2040,7 +2039,7 @@ def list_picker_providers(\n     current_base_url: str = \"\",\n     user_providers: dict = None,\n     custom_providers: list | None = None,\n-    max_models: int = 8,\n+    max_models: int | None = None,\n     current_model: str = \"\",\n ) -> List[dict]:\n     \"\"\"Interactive-picker variant of :func:`list_authenticated_providers`.","@@ -2083,7 +2082,7 @@ def list_picker_providers(\n             except Exception:\n                 live_ids = list(p.get(\"models\", []))\n             p = dict(p)\n-            p[\"models\"] = live_ids[:max_models]\n+            p[\"models\"] = live_ids[:max_models] if max_models is not None else live_ids\n             p[\"total_models\"] = len(live_ids)\n \n         has_models = bool(p.get(\"models\"))"]},{"file":"hermes_cli/web_server.py","hunks":["@@ -3323,7 +3323,6 @@ def get_model_options(profile: Optional[str] = None):\n         with _profile_scope(profile):\n             return build_models_payload(\n                 load_picker_context(),\n-                max_models=50,\n                 include_unconfigured=True,\n                 picker_hints=True,\n                 canonical_order=True,","@@ -3398,7 +3397,7 @@ def get_recommended_default_model(provider: str = \"\"):\n     try:\n         from hermes_cli.inventory import build_models_payload, load_picker_context\n \n-        payload = build_models_payload(load_picker_context(), max_models=50)\n+        payload = build_models_payload(load_picker_context())\n         for row in payload.get(\"providers\", []):\n             if str(row.get(\"slug\", \"\")).lower() == slug:\n                 models = row.get(\"models\") or []"]},{"file":"tests/hermes_cli/test_inventory.py","hunks":["@@ -660,3 +660,31 @@ def test_two_custom_providers_with_overlap_both_survive():\n     assert a_row[\"total_models\"] == 2\n     assert b_row[\"total_models\"] == 2\n \n+\n+def test_build_models_payload_no_max_models_returns_full_list():\n+    \"\"\"When max_models is not passed (None), build_models_payload must\n+    return the full model list — not truncate to the old default of 50.\n+    Regression for #48279: Kilo Gateway picker was capped at 50 of 336\n+    models, making most models undiscoverable via search.\"\"\"\n+    full_models = [f\"model-{i}\" for i in range(100)]\n+    rows = [\n+        {\n+            \"slug\": \"kilocode\",\n+            \"name\": \"Kilo Code\",\n+            \"models\": full_models,\n+            \"total_models\": len(full_models),\n+            \"is_current\": False,\n+            \"is_user_defined\": False,\n+            \"source\": \"built-in\",\n+        },\n+    ]\n+    ctx = _empty_ctx()\n+    with _list_auth_returning(rows):\n+        # No max_models argument — should return all 100 models\n+        payload = build_models_payload(ctx)\n+\n+    kilo_row = next(r for r in payload[\"providers\"] if r[\"slug\"] == \"kilocode\")\n+    assert kilo_row[\"models\"] == full_models\n+    assert kilo_row[\"total_models\"] == 100\n+    assert len(kilo_row[\"models\"]) == 100\n+"]},{"file":"tests/hermes_cli/test_model_catalog.py","hunks":["@@ -423,6 +423,71 @@ def test_picker_nous_row_uses_curated_list(self, tmp_path, monkeypatch):\n         assert nous_row is not None, \"nous row must appear when authed\"\n         assert nous_row[\"models\"] == expected\n \n+    def test_picker_max_models_cap_semantics(self, tmp_path, monkeypatch):\n+        \"\"\"The cap argument has three distinct meanings on the real slicing\n+        path: ``None`` = unlimited (the cap-removal fix, #48297), ``0`` = no\n+        models (preserved for slug-only callers), an int N = first N. Guards\n+        the ``is not None`` distinction the cap-removal follow-up introduced —\n+        a ``if max_models`` (falsy) check would conflate ``0`` with unlimited.\n+        \"\"\"\n+        import importlib\n+        from hermes_cli import model_catalog\n+        from hermes_cli.models import get_curated_nous_model_ids\n+        importlib.reload(model_catalog)\n+        try:\n+            from hermes_cli.model_switch import (\n+                list_authenticated_providers,\n+                list_picker_providers,\n+            )\n+\n+            active_home = Path(os.environ[\"HERMES_HOME\"])\n+            (active_home / \"auth.json\").write_text(\n+                json.dumps(\n+                    {\n+                        \"providers\": {\"nous\": {\"access_token\": \"fake\"}},\n+                        \"credential_pool\": {},\n+                    }\n+                )\n+            )\n+            with patch.object(\n+                model_catalog, \"_fetch_manifest\", return_value=_valid_manifest()\n+            ), patch(\"hermes_cli.models.check_nous_free_tier\", return_value=False), patch(\n+                \"hermes_cli.models.union_with_portal_free_recommendations\",\n+                side_effect=lambda ids, *a, **k: (ids, {}),\n+            ), patch(\n+                \"hermes_cli.models.union_with_portal_paid_recommendations\",\n+                side_effect=lambda ids, *a, **k: (ids, {}),\n+            ):\n+                expected = get_curated_nous_model_ids()\n+                full = list_picker_providers(current_provider=\"nous\", max_models=None)\n+                one = list_picker_providers(current_provider=\"nous\", max_models=1)\n+                # 0 is exercised on list_authenticated_providers (the slug-only\n+                # path); the picker variant drops empty-model rows entirely, so\n+                # the empty-list contract lives on the auth-providers call.\n+                zero = list_authenticated_providers(\n+                    current_provider=\"nous\", max_models=0\n+                )\n+        finally:\n+            model_catalog.reset_cache()\n+\n+        def _nous(rows):\n+            return next((r for r in rows if r[\"slug\"] == \"nous\"), None)\n+\n+        # Only meaningful when the curated list actually exceeds 1 entry.\n+        assert len(expected) > 1, \"test needs a multi-model curated nous list\"\n+\n+        full_row = _nous(full)\n+        assert full_row is not None and full_row[\"models\"] == expected\n+\n+        one_row = _nous(one)\n+        assert one_row is not None and one_row[\"models\"] == expected[:1]\n+\n+        zero_row = _nous(zero)\n+        # 0 means an empty model list — NOT unlimited. total_models still real.\n+        assert zero_row is not None\n+        assert zero_row[\"models\"] == []\n+        assert zero_row[\"total_models\"] == len(expected)\n+\n \n # -----------------------------------------------------------------------------\n # Drift guard — prevent the in-repo curated lists from going out of sync with"]},{"file":"tui_gateway/server.py","hunks":["@@ -9280,7 +9280,6 @@ def _(rid, params: dict) -> dict:\n             canonical_order=True,\n             pricing=True,\n             capabilities=True,\n-            max_models=50,\n         )\n         return _ok(rid, payload)\n     except Exception as e:\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}