{"repo":"NousResearch/hermes-agent","public_id":"6999","display_label":"Custom provider models missing or incomplete in model picker","summary":"These issues and fixes concern custom/user-defined providers not surfacing their full model lists in the /model picker across CLI, TUI, and desktop interfaces. Work spans model discovery, normalization, deduplication, and merging of configured and live-discovered models.","subsystem":"apps/desktop","counts":{"members":76,"open_issues":7,"open_issues_distinct":7,"open_prs":4,"closed_issues":28,"merged_prs":6,"attempt_prs":35},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":12557},"resolution":{"state":"partially_resolved","by":"pr-12557,pr-21211,pr-61928"},"freshness":{"state":"verified","last_verified_at":"2026-07-10T07:40:31+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.027171929824561392},"fix_edges":[{"issue_number":6862,"pr_number":21211,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":7103,"pr_number":17023,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":7103,"pr_number":21211,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":8216,"pr_number":12557,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":8216,"pr_number":14495,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":8758,"pr_number":12557,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":8758,"pr_number":21211,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":9148,"pr_number":12557,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":11017,"pr_number":12557,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":11017,"pr_number":14495,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":11499,"pr_number":12557,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":11677,"pr_number":12557,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":19071,"pr_number":21211,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":40480,"pr_number":45597,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":40542,"pr_number":61928,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":44513,"pr_number":45597,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":6999,"title":"Bug: /model shows only default_model for user-defined providers (providers: config)","url":null,"state":"closed"},{"kind":"issue","number":7103,"title":"Support dynamic model discovery via /v1/models for user-defined providers","url":null,"state":"closed"},{"kind":"issue","number":8216,"title":"Bug: /model picker ignores custom_providers.models and only shows one model","url":null,"state":"closed"},{"kind":"issue","number":8758,"title":"Bug: named custom provider model discovery ignores models mapping and probe fallback","url":null,"state":"open"},{"kind":"issue","number":9140,"title":"fix(model): /model picker shows 0 models for custom_providers with models: field","url":null,"state":"closed"},{"kind":"issue","number":9148,"title":"Bug: /model picker shows 0 models for custom_providers with models: dict","url":null,"state":"closed"},{"kind":"issue","number":10011,"title":"Auto-discover models from custom provider endpoints in /model picker","url":null,"state":"closed"},{"kind":"issue","number":11499,"title":"[Bug]: Telegram model picker does not list models from providers.custom.models dict","url":null,"state":"closed"},{"kind":"issue","number":11677,"title":"[Bug]: Custom Model Provider is not Enumerating Models for Model Picker, only the Default Model","url":null,"state":"closed"},{"kind":"issue","number":16438,"title":"[Bug]:  TUI /model picker does not show custom_providers defined in config.yaml","url":null,"state":"closed"},{"kind":"issue","number":19071,"title":"custom_providers: /model picker ignores live /v1/models endpoint, shows stale static subset","url":null,"state":"closed"},{"kind":"issue","number":20582,"title":"Model picker only shows one model for custom providers","url":null,"state":"open"},{"kind":"issue","number":30202,"title":"Feature: /model command should enumerate custom/local provider models","url":null,"state":"closed"},{"kind":"issue","number":33595,"title":"[Bug]: Telegram model picker skips live /v1/models discovery for providers: entries without api_key","url":null,"state":"closed"},{"kind":"issue","number":40480,"title":"Desktop App: Custom provider models not shown in model selection dropdown","url":null,"state":"closed"},{"kind":"issue","number":40542,"title":"[Bug]: /model picker only shows 1 model for local custom providers without api_key (while hermes model CLI shows all)","url":null,"state":"closed"},{"kind":"issue","number":43409,"title":"feat: /model interactive picker should list custom_providers models","url":null,"state":"closed"},{"kind":"issue","number":44283,"title":"feat: Show custom_providers in model/provider selector (desktop + CLI)","url":null,"state":"closed"},{"kind":"issue","number":44434,"title":"feat: Show custom_providers in model/provider selector (desktop + CLI)","url":null,"state":"closed"},{"kind":"issue","number":44435,"title":"feat: Show custom_providers in model/provider selector (desktop + CLI)","url":null,"state":"closed"},{"kind":"issue","number":47042,"title":"[BUG] Desktop model picker hides custom providers due to is_aggregator() false positive in dedup logic","url":null,"state":"closed"},{"kind":"issue","number":47124,"title":"[Bug]: Custom endpoints do not appear in the Agent because their models are wiped by the aggregator deduplication logic","url":null,"state":"closed"},{"kind":"issue","number":47132,"title":"bug(inventory): named custom providers lose all models in GUI picker after 60cc42e38","url":null,"state":"closed"},{"kind":"issue","number":47155,"title":"Regression: /model hides custom:* provider after deduplication empties its model list","url":null,"state":"closed"},{"kind":"issue","number":47258,"title":"UI model picker shows 0 models for custom_providers entries","url":null,"state":"closed"},{"kind":"issue","number":47386,"title":"Custom providers with models: list show 0 models in Desktop/TUI model picker","url":null,"state":"closed"},{"kind":"issue","number":47402,"title":"Model picker shows 0 models for custom: providers due to is_aggregator() treating all custom: slugs as aggregators","url":null,"state":"closed"},{"kind":"issue","number":47704,"title":"[BUG] Model picker dedup overfilters non-routing aggregators (opencode-go, huggingface, novita, etc.)","url":null,"state":"open"},{"kind":"issue","number":48885,"title":"[Bug]: Desktop UI model picker does not show custom/local provider models (hounddog_local / LM Studio)","url":null,"state":"open"},{"kind":"issue","number":48990,"title":"Bug: custom_providers models silently stripped from /model picker","url":null,"state":"closed"},{"kind":"issue","number":49126,"title":"[Bug]: Global aggregator dedup strips overlapping models from named providers configured under providers:","url":null,"state":"open"},{"kind":"issue","number":49656,"title":"Desktop: custom_providers not shown in model provider selector UI","url":null,"state":"open"},{"kind":"issue","number":55573,"title":"[Feature]: Support multi-model configuration/selection for custom OpenAI-compatible providers","url":null,"state":"closed"},{"kind":"issue","number":59560,"title":"[Bug]: custom_providers.models list is ignored — combo/9router models are not detected/selectable in Hermes Desktop","url":null,"state":"closed"},{"kind":"issue","number":63211,"title":"Model picker hides custom base_url models until manual refresh; stale providers remain visible","url":null,"state":"open"},{"kind":"pr","number":7132,"title":"feat: auto-discover models for custom/user-defined providers via /v1/models","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#21211","number":21211},{"kind":"pr","value":"#21211","number":21211},{"kind":"pr","value":"#17023","number":17023}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":8223,"title":"fix(cli): include custom provider model maps in /model picker","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":8770,"title":"fix(models): list named custom provider models","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#21211","number":21211},{"kind":"pr","value":"#21211","number":21211},{"kind":"pr","value":"#19072","number":19072}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":9134,"title":"fix(model): pass api_mode through validation chain and support custom:name:model triple syntax","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":9152,"title":"fix(model): api_mode propagation, triple syntax, and custom provider model listing","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":10326,"title":"fix(config): support models list in custom_providers picker (#8216)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#12557","number":12557}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":11403,"title":"fix(model_switch): read models dict keys from custom_providers entries","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#12557","number":12557}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":11505,"title":"fix(cli): support custom provider model dictionaries in model picker","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#11499","number":11499}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":11534,"title":"fix(model): support providers.*.models dict in Telegram /model picker","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#12557","number":12557},{"kind":"pr","value":"#12505","number":12505}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":11681,"title":"fix(model-picker): expose full custom provider model maps (#11677)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#11677","number":11677}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":11968,"title":"fix: include all models from custom_providers[].models in model picker","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#12557","number":12557}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":12505,"title":"fix(model_switch): enumerate dict-format models in /model picker","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#12557","number":12557}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":12557,"title":"fix(model_switch): enumerate dict-format models in /model picker + section-3 refinements","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/model_switch.py","scripts/release.py","tests/hermes_cli/test_model_switch_custom_providers.py","tests/hermes_cli/test_user_providers_model_switch.py"],"fix_hunks":[{"file":"hermes_cli/model_switch.py","hunks":["@@ -1039,17 +1039,38 @@ def list_authenticated_providers(\n         for ep_name, ep_cfg in user_providers.items():\n             if not isinstance(ep_cfg, dict):\n                 continue\n+            # Skip if this slug was already emitted (e.g. canonical provider\n+            # with the same name) or will be picked up by section 4.\n+            if ep_name.lower() in seen_slugs:\n+                continue\n             display_name = ep_cfg.get(\"name\", \"\") or ep_name\n-            api_url = ep_cfg.get(\"api\", \"\") or ep_cfg.get(\"url\", \"\") or \"\"\n-            default_model = ep_cfg.get(\"default_model\", \"\")\n+            # ``base_url`` is Hermes's canonical write key (matches\n+            # custom_providers and _save_custom_provider); ``api`` / ``url``\n+            # remain as fallbacks for hand-edited / legacy configs.\n+            api_url = (\n+                ep_cfg.get(\"base_url\", \"\")\n+                or ep_cfg.get(\"api\", \"\")\n+                or ep_cfg.get(\"url\", \"\")\n+                or \"\"\n+            )\n+            # ``default_model`` is the legacy key; ``model`` matches what\n+            # custom_providers entries use, so accept either.\n+            default_model = ep_cfg.get(\"default_model\", \"\") or ep_cfg.get(\"model\", \"\")\n \n             # Build models list from both default_model and full models array\n             models_list = []\n             if default_model:\n                 models_list.append(default_model)\n-            # Also include the full models list from config\n+            # Also include the full models list from config.\n+            # Hermes writes ``models:`` as a dict keyed by model id\n+            # (see hermes_cli/main.py::_save_custom_provider); older\n+            # configs or hand-edited files may still use a list.\n             cfg_models = ep_cfg.get(\"models\", [])\n-            if isinstance(cfg_models, list):\n+            if isinstance(cfg_models, dict):\n+                for m in cfg_models:\n+                    if m and m not in models_list:\n+                        models_list.append(m)\n+            elif isinstance(cfg_models, list):\n                 for m in cfg_models:\n                     if m and m not in models_list:\n                         models_list.append(m)","@@ -1066,6 +1087,7 @@ def list_authenticated_providers(\n                 \"source\": \"user-config\",\n                 \"api_url\": api_url,\n             })\n+            seen_slugs.add(ep_name.lower())\n \n     # --- 4. Saved custom providers from config ---\n     # Each ``custom_providers`` entry represents one model under a named","@@ -1100,10 +1122,27 @@ def list_authenticated_providers(\n                     \"api_url\": api_url,\n                     \"models\": [],\n                 }\n+            # The singular ``model:`` field only holds the currently\n+            # active model. Hermes's own writer (main.py::_save_custom_provider)\n+            # stores every configured model as a dict under ``models:``;\n+            # downstream readers (agent/models_dev.py, gateway/run.py,\n+            # run_agent.py, hermes_cli/config.py) already consume that dict.\n+            # The /model picker previously ignored it, so multi-model\n+            # custom providers appeared to have only the active model.\n             default_model = (entry.get(\"model\") or \"\").strip()\n             if default_model and default_model not in groups[slug][\"models\"]:\n                 groups[slug][\"models\"].append(default_model)\n \n+            cfg_models = entry.get(\"models\", {})\n+            if isinstance(cfg_models, dict):\n+                for m in cfg_models:\n+                    if m and m not in groups[slug][\"models\"]:\n+                        groups[slug][\"models\"].append(m)\n+            elif isinstance(cfg_models, list):\n+                for m in cfg_models:\n+                    if m and m not in groups[slug][\"models\"]:\n+                        groups[slug][\"models\"].append(m)\n+\n         for slug, grp in groups.items():\n             if slug.lower() in seen_slugs:\n                 continue"]},{"file":"scripts/release.py","hunks":["@@ -177,6 +177,7 @@\n     \"[redacted:email]\": \"duerzy\",\n     \"[redacted:email]\": \"emozilla\",\n     \"[redacted:email]\": \"fancydirty\",\n+    \"[redacted:email]\": \"farion1231\",\n     \"[redacted:email]\": \"flobo3\",\n     \"[redacted:email]\": \"pefontana\",\n     \"[redacted:email]\": \"fxfitz\","]},{"file":"tests/hermes_cli/test_model_switch_custom_providers.py","hunks":["@@ -156,3 +156,100 @@ def test_list_deduplicates_same_model_in_group(monkeypatch):\n     assert len(my_rows) == 1\n     assert my_rows[0][\"models\"] == [\"llama3\", \"mistral\"]\n     assert my_rows[0][\"total_models\"] == 2\n+\n+\n+def test_list_enumerates_dict_format_models_alongside_default(monkeypatch):\n+    \"\"\"custom_providers entry with dict-format ``models:`` plus singular\n+    ``model:`` should surface the default and every dict key.\n+\n+    Regression: Hermes's own writer stores configured models as a dict\n+    keyed by model id, but the /model picker previously only honored the\n+    singular ``model:`` field, so multi-model custom providers appeared\n+    to have only the active model.\n+    \"\"\"\n+    monkeypatch.setattr(\"agent.models_dev.fetch_models_dev\", lambda: {})\n+    monkeypatch.setattr(providers_mod, \"HERMES_OVERLAYS\", {})\n+\n+    providers = list_authenticated_providers(\n+        current_provider=\"openai-codex\",\n+        user_providers={},\n+        custom_providers=[\n+            {\n+                \"name\": \"DeepSeek\",\n+                \"base_url\": \"https://api.deepseek.com\",\n+                \"api_mode\": \"chat_completions\",\n+                \"model\": \"deepseek-chat\",\n+                \"models\": {\n+                    \"deepseek-chat\": {\"context_length\": 128000},\n+                    \"deepseek-reasoner\": {\"context_length\": 128000},\n+                },\n+            }\n+        ],\n+        max_models=50,\n+    )\n+\n+    ds_rows = [p for p in providers if p[\"name\"] == \"DeepSeek\"]\n+    assert len(ds_rows) == 1\n+    assert ds_rows[0][\"models\"] == [\"deepseek-chat\", \"deepseek-reasoner\"]\n+    assert ds_rows[0][\"total_models\"] == 2\n+\n+\n+def test_list_enumerates_dict_format_models_without_singular_model(monkeypatch):\n+    \"\"\"Dict-format ``models:`` with no singular ``model:`` should still\n+    enumerate every dict key (previously the picker reported 0 models).\"\"\"\n+    monkeypatch.setattr(\"agent.models_dev.fetch_models_dev\", lambda: {})\n+    monkeypatch.setattr(providers_mod, \"HERMES_OVERLAYS\", {})\n+\n+    providers = list_authenticated_providers(\n+        current_provider=\"openai-codex\",\n+        user_providers={},\n+        custom_providers=[\n+            {\n+                \"name\": \"Thor\",\n+                \"base_url\": \"http://thor.lab:8337/v1\",\n+                \"models\": {\n+                    \"gemma-4-26B-A4B-it-MXFP4_MOE\": {\"context_length\": 262144},\n+                    \"Qwen3.5-35B-A3B-MXFP4_MOE\": {\"context_length\": 262144},\n+                    \"gemma-4-31B-it-Q4_K_M\": {\"context_length\": 262144},\n+                },\n+            }\n+        ],\n+        max_models=50,\n+    )\n+\n+    thor_rows = [p for p in providers if p[\"name\"] == \"Thor\"]\n+    assert len(thor_rows) == 1\n+    assert set(thor_rows[0][\"models\"]) == {\n+        \"gemma-4-26B-A4B-it-MXFP4_MOE\",\n+        \"Qwen3.5-35B-A3B-MXFP4_MOE\",\n+        \"gemma-4-31B-it-Q4_K_M\",\n+    }\n+    assert thor_rows[0][\"total_models\"] == 3\n+\n+\n+def test_list_dedupes_dict_model_matching_singular_default(monkeypatch):\n+    \"\"\"When the singular ``model:`` is also a key in the ``models:`` dict,\n+    it must appear exactly once in the picker.\"\"\"\n+    monkeypatch.setattr(\"agent.models_dev.fetch_models_dev\", lambda: {})\n+    monkeypatch.setattr(providers_mod, \"HERMES_OVERLAYS\", {})\n+\n+    providers = list_authenticated_providers(\n+        current_provider=\"openai-codex\",\n+        user_providers={},\n+        custom_providers=[\n+            {\n+                \"name\": \"DeepSeek\",\n+                \"base_url\": \"https://api.deepseek.com\",\n+                \"model\": \"deepseek-chat\",\n+                \"models\": {\n+                    \"deepseek-chat\": {\"context_length\": 128000},\n+                    \"deepseek-reasoner\": {\"context_length\": 128000},\n+                },\n+            }\n+        ],\n+        max_models=50,\n+    )\n+\n+    ds_rows = [p for p in providers if p[\"name\"] == \"DeepSeek\"]\n+    assert ds_rows[0][\"models\"].count(\"deepseek-chat\") == 1\n+    assert ds_rows[0][\"models\"] == [\"deepseek-chat\", \"deepseek-reasoner\"]"]},{"file":"tests/hermes_cli/test_user_providers_model_switch.py","hunks":["@@ -86,6 +86,117 @@ def test_list_authenticated_providers_dedupes_models_when_default_in_list(monkey\n     assert user_prov[\"models\"].count(\"model-a\") == 1, \"model-a should not be duplicated\"\n \n \n+def test_list_authenticated_providers_enumerates_dict_format_models(monkeypatch):\n+    \"\"\"providers: dict entries with ``models:`` as a dict keyed by model id\n+    (canonical Hermes write format) should surface every key in the picker.\n+\n+    Regression: the ``providers:`` dict path previously only accepted\n+    list-format ``models:`` and silently dropped dict-format entries,\n+    even though Hermes's own writer and downstream readers use dict format.\n+    \"\"\"\n+    monkeypatch.setattr(\"agent.models_dev.fetch_models_dev\", lambda: {})\n+    monkeypatch.setattr(\"hermes_cli.providers.HERMES_OVERLAYS\", {})\n+\n+    user_providers = {\n+        \"local-ollama\": {\n+            \"name\": \"Local Ollama\",\n+            \"api\": \"http://localhost:11434/v1\",\n+            \"default_model\": \"minimax-m2.7:cloud\",\n+            \"models\": {\n+                \"minimax-m2.7:cloud\": {\"context_length\": 196608},\n+                \"kimi-k2.5:cloud\": {\"context_length\": 200000},\n+                \"glm-5.1:cloud\": {\"context_length\": 202752},\n+            },\n+        }\n+    }\n+\n+    providers = list_authenticated_providers(\n+        current_provider=\"local-ollama\",\n+        user_providers=user_providers,\n+        custom_providers=[],\n+        max_models=50,\n+    )\n+\n+    user_prov = next(\n+        (p for p in providers if p.get(\"is_user_defined\") and p[\"slug\"] == \"local-ollama\"),\n+        None,\n+    )\n+\n+    assert user_prov is not None\n+    assert user_prov[\"total_models\"] == 3\n+    assert user_prov[\"models\"] == [\n+        \"minimax-m2.7:cloud\",\n+        \"kimi-k2.5:cloud\",\n+        \"glm-5.1:cloud\",\n+    ]\n+\n+\n+def test_list_authenticated_providers_dict_models_without_default_model(monkeypatch):\n+    \"\"\"Dict-format ``models:`` without a ``default_model`` must still expose\n+    every dict key, not collapse to an empty list.\"\"\"\n+    monkeypatch.setattr(\"agent.models_dev.fetch_models_dev\", lambda: {})\n+    monkeypatch.setattr(\"hermes_cli.providers.HERMES_OVERLAYS\", {})\n+\n+    user_providers = {\n+        \"multimodel\": {\n+            \"api\": \"http://example.com/v1\",\n+            \"models\": {\n+                \"alpha\": {\"context_length\": 8192},\n+                \"beta\": {\"context_length\": 16384},\n+            },\n+        }\n+    }\n+\n+    providers = list_authenticated_providers(\n+        current_provider=\"\",\n+        user_providers=user_providers,\n+        custom_providers=[],\n+    )\n+\n+    user_prov = next(\n+        (p for p in providers if p.get(\"is_user_defined\") and p[\"slug\"] == \"multimodel\"),\n+        None,\n+    )\n+\n+    assert user_prov is not None\n+    assert user_prov[\"total_models\"] == 2\n+    assert set(user_prov[\"models\"]) == {\"alpha\", \"beta\"}\n+\n+\n+def test_list_authenticated_providers_dict_models_dedupe_with_default(monkeypatch):\n+    \"\"\"When ``default_model`` is also a key in the ``models:`` dict, it must\n+    appear exactly once (list already had this for list-format models).\"\"\"\n+    monkeypatch.setattr(\"agent.models_dev.fetch_models_dev\", lambda: {})\n+    monkeypatch.setattr(\"hermes_cli.providers.HERMES_OVERLAYS\", {})\n+\n+    user_providers = {\n+        \"my-provider\": {\n+            \"api\": \"http://example.com/v1\",\n+            \"default_model\": \"model-a\",\n+            \"models\": {\n+                \"model-a\": {\"context_length\": 8192},\n+                \"model-b\": {\"context_length\": 16384},\n+                \"model-c\": {\"context_length\": 32768},\n+            },\n+        }\n+    }\n+\n+    providers = list_authenticated_providers(\n+        current_provider=\"my-provider\",\n+        user_providers=user_providers,\n+        custom_providers=[],\n+    )\n+\n+    user_prov = next(\n+        (p for p in providers if p.get(\"is_user_defined\")),\n+        None,\n+    )\n+\n+    assert user_prov is not None\n+    assert user_prov[\"total_models\"] == 3\n+    assert user_prov[\"models\"].count(\"model-a\") == 1\n+\n+\n def test_list_authenticated_providers_fallback_to_default_only(monkeypatch):\n     \"\"\"When no models array is provided, should fall back to default_model.\"\"\"\n     monkeypatch.setattr(\"agent.models_dev.fetch_models_dev\", lambda: {})","@@ -116,6 +227,83 @@ def test_list_authenticated_providers_fallback_to_default_only(monkeypatch):\n     assert user_prov[\"models\"] == [\"single-model\"]\n \n \n+def test_list_authenticated_providers_accepts_base_url_and_singular_model(monkeypatch):\n+    \"\"\"providers: dict entries written in canonical Hermes shape\n+    (``base_url`` + singular ``model``) should resolve the same as the\n+    legacy ``api`` + ``default_model`` shape.\n+\n+    Regression: section 3 previously only read ``api``/``url`` and\n+    ``default_model``, so new-shape entries written by Hermes's own writer\n+    surfaced with empty ``api_url`` and no default.\n+    \"\"\"\n+    monkeypatch.setattr(\"agent.models_dev.fetch_models_dev\", lambda: {})\n+    monkeypatch.setattr(\"hermes_cli.providers.HERMES_OVERLAYS\", {})\n+\n+    user_providers = {\n+        \"custom\": {\n+            \"base_url\": \"http://example.com/v1\",\n+            \"model\": \"gpt-5.4\",\n+            \"models\": {\n+                \"gpt-5.4\": {},\n+                \"grok-4.20-beta\": {},\n+                \"minimax-m2.7\": {},\n+            },\n+        }\n+    }\n+\n+    providers = list_authenticated_providers(\n+        current_provider=\"custom\",\n+        user_providers=user_providers,\n+        custom_providers=[],\n+        max_models=50,\n+    )\n+\n+    custom = next((p for p in providers if p[\"slug\"] == \"custom\"), None)\n+    assert custom is not None\n+    assert custom[\"api_url\"] == \"http://example.com/v1\"\n+    assert custom[\"models\"] == [\"gpt-5.4\", \"grok-4.20-beta\", \"minimax-m2.7\"]\n+    assert custom[\"total_models\"] == 3\n+\n+\n+def test_list_authenticated_providers_dedupes_when_user_and_custom_overlap(monkeypatch):\n+    \"\"\"When the same slug appears in both ``providers:`` dict and\n+    ``custom_providers:`` list, emit exactly one row (providers: dict wins\n+    since it is processed first).\n+\n+    Regression: section 3 previously had no ``seen_slugs`` check, so\n+    overlapping entries produced two picker rows for the same provider.\n+    \"\"\"\n+    monkeypatch.setattr(\"agent.models_dev.fetch_models_dev\", lambda: {})\n+    monkeypatch.setattr(\"hermes_cli.providers.HERMES_OVERLAYS\", {})\n+\n+    providers = list_authenticated_providers(\n+        current_provider=\"custom\",\n+        user_providers={\n+            \"custom\": {\n+                \"base_url\": \"http://example.com/v1\",\n+                \"model\": \"gpt-5.4\",\n+                \"models\": {\n+                    \"gpt-5.4\": {},\n+                    \"grok-4.20-beta\": {},\n+                },\n+            }\n+        },\n+        custom_providers=[\n+            {\n+                \"name\": \"custom\",\n+                \"base_url\": \"http://example.com/v1\",\n+                \"model\": \"legacy-only-model\",\n+            }\n+        ],\n+        max_models=50,\n+    )\n+\n+    matches = [p for p in providers if p[\"slug\"] == \"custom\"]\n+    assert len(matches) == 1\n+    # providers: dict wins — legacy-only-model is suppressed.\n+    assert matches[0][\"models\"] == [\"gpt-5.4\", \"grok-4.20-beta\"]\n+\n+\n # =============================================================================\n # Tests for _get_named_custom_provider with providers: dict\n # =============================================================================\n"]}]},{"kind":"pr","number":14495,"title":"fix(config): preserve list-format models in custom_providers normalize (#13509)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/config.py","scripts/release.py","tests/hermes_cli/test_provider_config_validation.py"],"fix_hunks":[{"file":"hermes_cli/config.py","hunks":["@@ -2055,6 +2055,14 @@ def _normalize_custom_provider_entry(\n     models = entry.get(\"models\")\n     if isinstance(models, dict) and models:\n         normalized[\"models\"] = models\n+    elif isinstance(models, list) and models:\n+        # Hand-edited configs (and older Hermes versions) write ``models`` as\n+        # a plain list of model ids. Preserve them by converting to the dict\n+        # shape downstream code expects; otherwise normalize silently drops\n+        # the list and /model shows the provider with (0) models.\n+        normalized[\"models\"] = {\n+            str(m): {} for m in models if isinstance(m, str) and m.strip()\n+        }\n \n     context_length = entry.get(\"context_length\")\n     if isinstance(context_length, int) and context_length > 0:"]},{"file":"scripts/release.py","hunks":["@@ -43,6 +43,7 @@\n     \"[redacted:email]\": \"teknium1\",\n     \"[redacted:email]\": \"teknium1\",\n     \"[redacted:email]\": \"teknium1\",\n+    \"[redacted:email]\": \"DrStrangerUJN\",\n     # contributors (from noreply pattern)\n     \"[redacted:email]\": \"xiaoqiang243\",\n     \"[redacted:email]\": \"snreynolds\","]},{"file":"tests/hermes_cli/test_provider_config_validation.py","hunks":["@@ -135,3 +135,48 @@ def test_no_name_returns_none(self):\n         }\n         result = _normalize_custom_provider_entry(entry, provider_key=\"\")\n         assert result is None\n+\n+    def test_models_list_converted_to_dict(self):\n+        \"\"\"List-format models should be preserved as an empty-value dict so\n+        /model picks them up instead of showing the provider with (0) models.\"\"\"\n+        entry = {\n+            \"name\": \"tencent-coding-plan\",\n+            \"base_url\": \"https://api.lkeap.cloud.tencent.com/coding/v3\",\n+            \"models\": [\"glm-5\", \"kimi-k2.5\", \"minimax-m2.5\"],\n+        }\n+        result = _normalize_custom_provider_entry(entry)\n+        assert result is not None\n+        assert result[\"models\"] == {\"glm-5\": {}, \"kimi-k2.5\": {}, \"minimax-m2.5\": {}}\n+\n+    def test_models_dict_preserved(self):\n+        \"\"\"Dict-format models should pass through unchanged.\"\"\"\n+        entry = {\n+            \"name\": \"acme\",\n+            \"base_url\": \"https://api.example.com/v1\",\n+            \"models\": {\"gpt-foo\": {\"context_length\": 32000}},\n+        }\n+        result = _normalize_custom_provider_entry(entry)\n+        assert result is not None\n+        assert result[\"models\"] == {\"gpt-foo\": {\"context_length\": 32000}}\n+\n+    def test_models_list_filters_empty_and_non_string(self):\n+        \"\"\"List entries that are empty strings or non-strings are skipped.\"\"\"\n+        entry = {\n+            \"name\": \"acme\",\n+            \"base_url\": \"https://api.example.com/v1\",\n+            \"models\": [\"valid\", \"\", None, 42, \"  \", \"also-valid\"],\n+        }\n+        result = _normalize_custom_provider_entry(entry)\n+        assert result is not None\n+        assert result[\"models\"] == {\"valid\": {}, \"also-valid\": {}}\n+\n+    def test_models_empty_list_omitted(self):\n+        \"\"\"Empty list (falsy) should not produce a models key.\"\"\"\n+        entry = {\n+            \"name\": \"acme\",\n+            \"base_url\": \"https://api.example.com/v1\",\n+            \"models\": [],\n+        }\n+        result = _normalize_custom_provider_entry(entry)\n+        assert result is not None\n+        assert \"models\" not in result\n"]}]},{"kind":"pr","number":15081,"title":"fix: support list format for custom_providers[].models in _normalize_custom_provider_entry","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#14495","number":14495},{"kind":"pr","value":"#15081","number":15081},{"kind":"pr","value":"#15081","number":15081},{"kind":"pr","value":"#14495","number":14495}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":16460,"title":"fix(cli): include custom_providers in list_available_providers for TUI /model picker","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":17023,"title":"fix(model): prefer live /v1/models for user-defined providers (salvage #16747)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/model_switch.py","scripts/release.py","tests/hermes_cli/test_user_providers_model_switch.py"],"fix_hunks":[{"file":"hermes_cli/model_switch.py","hunks":["@@ -1346,8 +1346,23 @@ def list_authenticated_providers(\n                     if fb:\n                         models_list = list(fb)\n \n-            # Try to probe /v1/models if URL is set (but don't block on it)\n-            # For now just show what we know from config\n+            # Prefer the endpoint's live /models list when credentials are\n+            # available. This keeps OpenAI-compatible relays (for example CRS)\n+            # in sync when the server catalog changes without requiring the\n+            # user to mirror every model into config.yaml.\n+            api_key = [redacted:secret-assignment]\"api_key\", \"\") or \"\").strip()\n+            if not api_key:\n+                key_env = str(ep_cfg.get(\"key_env\", \"\") or \"\").strip()\n+                api_key = [redacted:secret-assignment] \"\").strip() if key_env else \"\"\n+            if api_url and api_key:\n+                try:\n+                    from hermes_cli.models import fetch_api_models\n+                    live_models = fetch_api_models(api_key, api_url)\n+                    if live_models:\n+                        models_list = live_models\n+                except Exception:\n+                    pass\n+\n             results.append({\n                 \"slug\": ep_name,\n                 \"name\": display_name,"]},{"file":"scripts/release.py","hunks":["@@ -53,6 +53,7 @@\n     \"[redacted:email]\": \"cyclingwithelephants\",\n     \"[redacted:email]\": \"liizfq\",\n     \"[redacted:email]\": \"dhabibi\",\n+    \"[redacted:email]\": \"JayGwod\",\n     \"[redacted:email]\": \"johnncenae\",\n     \"[redacted:email]\": \"ThomassJonax\",\n     \"[redacted:email]\": \"yes999zc\","]},{"file":"tests/hermes_cli/test_user_providers_model_switch.py","hunks":["@@ -131,6 +131,55 @@ def test_list_authenticated_providers_enumerates_dict_format_models(monkeypatch)\n     ]\n \n \n+def test_list_authenticated_providers_uses_live_models_for_user_provider(monkeypatch):\n+    \"\"\"User-defined OpenAI-compatible providers should prefer live /models.\n+\n+    Regression: CRS-style providers with a stale config ``models:`` dict kept\n+    showing only the configured subset in the /model picker, even though their\n+    /v1/models endpoint exposed newly added models.\n+    \"\"\"\n+    monkeypatch.setattr(\"agent.models_dev.fetch_models_dev\", lambda: {})\n+    monkeypatch.setattr(\"hermes_cli.providers.HERMES_OVERLAYS\", {})\n+    monkeypatch.setenv(\"CRS_TEST_KEY\", \"sk-test\")\n+\n+    calls = []\n+\n+    def fake_fetch_api_models(api_key, base_url):\n+        calls.append((api_key, base_url))\n+        return [\"old-configured-model\", \"new-live-model\"]\n+\n+    monkeypatch.setattr(\"hermes_cli.models.fetch_api_models\", fake_fetch_api_models)\n+\n+    user_providers = {\n+        \"crs-henkee\": {\n+            \"name\": \"CRS Henkee\",\n+            \"base_url\": \"http://127.0.0.1:3000/api/v1\",\n+            \"key_env\": \"CRS_TEST_KEY\",\n+            \"model\": \"old-configured-model\",\n+            \"models\": {\n+                \"old-configured-model\": {\"context_length\": 200000},\n+            },\n+        }\n+    }\n+\n+    providers = list_authenticated_providers(\n+        current_provider=\"crs-henkee\",\n+        user_providers=user_providers,\n+        custom_providers=[],\n+        max_models=50,\n+    )\n+\n+    user_prov = next(\n+        (p for p in providers if p.get(\"is_user_defined\") and p[\"slug\"] == \"crs-henkee\"),\n+        None,\n+    )\n+\n+    assert user_prov is not None\n+    assert calls == [(\"sk-test\", \"http://127.0.0.1:3000/api/v1\")]\n+    assert user_prov[\"models\"] == [\"old-configured-model\", \"new-live-model\"]\n+    assert user_prov[\"total_models\"] == 2\n+\n+\n def test_list_authenticated_providers_dict_models_without_default_model(monkeypatch):\n     \"\"\"Dict-format ``models:`` without a ``default_model`` must still expose\n     every dict key, not collapse to an empty list.\"\"\"\n"]}]},{"kind":"pr","number":19072,"title":"fix(model_switch): live model discovery for custom_providers in /model picker","url":null,"state":"closed","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":20751,"title":"fix: live model discovery for custom providers in /model picker","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19072","number":19072},{"kind":"pr","value":"#20763","number":20763},{"kind":"pr","value":"#20582","number":20582}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":20760,"title":"fix(cli): probe /v1/models for custom providers in model picker","url":null,"state":"closed","close_reason":{"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":20763,"title":"fix: live model discovery for custom providers in /model picker","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":20919,"title":"fix(models): show configured custom provider model in /model picker","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":21211,"title":"fix(model_switch): live model discovery for custom_providers in /model picker (salvage #19072)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/model_switch.py","tests/hermes_cli/test_model_switch_custom_providers.py"],"fix_hunks":[{"file":"hermes_cli/model_switch.py","hunks":["@@ -1637,7 +1637,8 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:\n                         groups[group_key][\"models\"].append(m)\n \n         _section4_emitted_slugs: set = set()\n-        for grp in groups.values():\n+        for grp_key, grp in groups.items():\n+            api_url, api_key = [redacted:secret-assignment]\n             slug = grp[\"slug\"]\n             # If the slug is already claimed by a built-in / overlay /\n             # user-provider row (sections 1-3), skip this custom group","@@ -1675,6 +1676,18 @@ def _has_aws_sdk_creds_for_listing(slug: str) -> bool:\n             _grp_url_norm = _pair_key[1]\n             if _grp_url_norm and _grp_url_norm in _builtin_endpoints:\n                 continue\n+            # Live model discovery from custom provider endpoints (matches\n+            # Section 3 behavior for user ``providers:`` entries).\n+            if api_url and api_key:\n+                try:\n+                    from hermes_cli.models import fetch_api_models\n+\n+                    live_models = fetch_api_models(api_key, api_url)\n+                    if live_models:\n+                        grp[\"models\"] = live_models\n+                        grp[\"total_models\"] = len(live_models)\n+                except Exception:\n+                    pass\n             results.append({\n                 \"slug\": slug,\n                 \"name\": grp[\"name\"],"]},{"file":"tests/hermes_cli/test_model_switch_custom_providers.py","hunks":["@@ -506,3 +506,64 @@ def _fake_fetch(api_key=None, base_url=None, timeout=5.0):\n     )\n \n     assert \"base_url\" not in captured\n+\n+\n+def test_custom_providers_uses_live_models_for_multi_model_endpoint(monkeypatch):\n+    \"\"\"Custom providers with api_key + base_url should prefer live /models.\n+\n+    Custom providers (section 4 of list_authenticated_providers) point at\n+    gateways like Bifrost that expose hundreds of models.  Reading only the\n+    static ``models:`` dict from config.yaml leaves the /model picker with\n+    a stale subset.  Live discovery fills the picker with all available\n+    models from the endpoint.\n+    \"\"\"\n+    monkeypatch.setattr(\"agent.models_dev.fetch_models_dev\", lambda: {})\n+    monkeypatch.setattr(\"hermes_cli.providers.HERMES_OVERLAYS\", {})\n+\n+    calls = []\n+\n+    def fake_fetch_api_models(api_key, base_url):\n+        calls.append((api_key, base_url))\n+        return [\"gateway-model-a\", \"gateway-model-b\", \"gateway-model-c\"]\n+\n+    monkeypatch.setattr(\"hermes_cli.models.fetch_api_models\", fake_fetch_api_models)\n+\n+    custom_providers = [\n+        {\n+            \"name\": \"my-gateway\",\n+            \"api_key\": \"sk-gateway-key\",\n+            \"base_url\": \"https://gateway.example.com/v1\",\n+            \"model\": \"gateway-model-a\",\n+            \"models\": {\n+                \"gateway-model-a\": {\"context_length\": 128000},\n+                \"gateway-model-b\": {\"context_length\": 128000},\n+            },\n+        }\n+    ]\n+\n+    providers = list_authenticated_providers(\n+        current_provider=\"openrouter\",\n+        current_base_url=\"https://openrouter.ai/api/v1\",\n+        custom_providers=custom_providers,\n+        max_models=50,\n+    )\n+\n+    gateway_prov = next(\n+        (\n+            p\n+            for p in providers\n+            if p.get(\"api_url\") == \"https://gateway.example.com/v1\"\n+        ),\n+        None,\n+    )\n+\n+    assert gateway_prov is not None, \"Custom provider group not found in results\"\n+    assert calls == [(\"sk-gateway-key\", \"https://gateway.example.com/v1\")], (\n+        \"fetch_api_models must be called with the custom provider's credentials\"\n+    )\n+    assert gateway_prov[\"models\"] == [\n+        \"gateway-model-a\",\n+        \"gateway-model-b\",\n+        \"gateway-model-c\",\n+    ], \"Live models must replace the static subset\"\n+    assert gateway_prov[\"total_models\"] == 3\n"]}]},{"kind":"pr","number":33601,"title":"fix(model): probe keyless user provider catalogs","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"1039e90b5e2a2ab8b005a780ca12b9cdd52362db","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":33966,"title":"fix(cli): probe live /v1/models for keyless providers in model picker","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#33601","number":33601},{"kind":"pr","value":"#33595","number":33595}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":40529,"title":"fix(cli): surface inline custom provider model in the picker (#40480)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"74c5158b1","number":0},{"kind":"pr","value":"#45597","number":45597},{"kind":"pr","value":"#40480","number":40480}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":40554,"title":"fix(cli): probe /v1/models live when custom provider has no api_key","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#61928","number":61928},{"kind":"pr","value":"#55158","number":55158},{"kind":"pr","value":"#40542","number":40542}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":45597,"title":"fix(model): show bare custom endpoints in gateway picker","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/model_switch.py","tests/hermes_cli/test_model_switch_custom_providers.py"]},{"kind":"pr","number":47050,"title":"fix(inventory): skip self-dedup when custom provider is also classified as aggregator","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"b7fa62c53019a7e44599de4f6cdd30071ee7b7e4","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":47133,"title":"fix(picker): skip user-defined providers in aggregator dedup (#47042)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#46921","number":46921},{"kind":"pr","value":"#47050","number":47050},{"kind":"pr","value":"#47042","number":47042},{"kind":"pr","value":"#45954","number":45954},{"kind":"pr","value":"#46829","number":46829}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":47727,"title":"fix(cli): narrow model picker dedup to routing aggregators only","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":48902,"title":"fix(model-picker): support model-keyed user providers","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":49150,"title":"fix(cli): preserve named provider aggregator models (#49126)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"a6ce9b2fbbdfbe1fecf6c72d28d02a72adccf82f","number":0},{"kind":"pr","value":"#49273","number":49273}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":49273,"title":"fix(cli): restrict aggregator dedup to custom proxy providers (#49126)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#49150","number":49150},{"kind":"pr","value":"#49126","number":49126},{"kind":"pr","value":"#49150","number":49150}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":55158,"title":"fix(model): probe no-key custom provider catalogs","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#61928","number":61928}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":59912,"title":"fix(model-switch): merge configured models with live /models discovery instead of replacing","url":null,"state":"closed","close_reason":{"category":"stale_abandoned","implication":"active"},"merged":false},{"kind":"pr","number":60320,"title":"fix(model-switch): merge configured custom_providers models with live discovery","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#36834","number":36834},{"kind":"pr","value":"#36834","number":36834},{"kind":"pr","value":"#59912","number":59912},{"kind":"pr","value":"#36834","number":36834},{"kind":"pr","value":"#59560","number":59560},{"kind":"pr","value":"#35145","number":35145}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":60503,"title":"fix(model-switch): merge configured custom_providers.models","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#59560","number":59560},{"kind":"pr","value":"#36834","number":36834}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":61606,"title":"feat(lmstudio): add local model management","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":61928,"title":"fix(model): discover no-key custom provider catalogs","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/model_switch.py","tests/hermes_cli/test_model_switch_custom_providers.py"]},{"kind":"pr","number":116765,"title":"fix(desktop): keep a configured custom provider visible in the model pickers","url":null,"state":"open","close_reason":{"category":"silent_close","implication":"active"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}