{"repo":"NousResearch/hermes-agent","public_id":"26146","display_label":"Model picker 50-entry cap hides provider models","summary":"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.","subsystem":"apps/desktop","counts":{"members":21,"open_issues":2,"open_issues_distinct":2,"open_prs":2,"closed_issues":6,"merged_prs":1,"attempt_prs":12},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":48611},"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.1019047619047619},"fix_edges":[{"issue_number":35443,"pr_number":48611,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":40601,"pr_number":48611,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":42270,"pr_number":48611,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":48279,"pr_number":48611,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":26146,"title":"[Bug]: /model slash command picker misses models visible in hermes setup model","url":null,"state":"closed"},{"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":"issue","number":59211,"title":"[Bug]: MoA configure shows only 2 NVIDIA models while normal model picker shows many NVIDIA models","url":null,"state":"open"},{"kind":"pr","number":26153,"title":"fix(model-picker): add minimax-cn to _MODELS_DEV_PREFERRED and include M2.7-highspeed","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":26166,"title":"fix(hermes_cli): add minimax-cn to _MODELS_DEV_PREFERRED","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#26153","number":26153},{"kind":"pr","value":"#26146","number":26146},{"kind":"pr","value":"#26153","number":26153}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"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":"#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},{"kind":"pr","value":"#40765","number":40765}],"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","implication":"superseded"},"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"]}]},{"kind":"pr","number":60764,"title":"fix(model): expand NVIDIA catalog fallback","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"240afd0b7","number":0},{"kind":"commit","value":"4043bb4ad","number":0}],"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}