{"repo":"NousResearch/hermes-agent","public_id":"6862","display_label":"Custom provider multi-model discovery and selection fixes","summary":"These issues and PRs address bugs and improvements around model selection and auto-discovery for custom/user-defined providers with multiple models, including fixes for incorrect model locking and live model enumeration via the /v1/models endpoint.","subsystem":null,"counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":2,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":7187},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-07T12:21:27+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.4},"fix_edges":[{"issue_number":6211,"pr_number":7187,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":6862,"pr_number":7187,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":6862,"pr_number":21211,"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":8758,"pr_number":21211,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false},{"issue_number":19071,"pr_number":21211,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":6862,"title":"hermes model locks to first model on multi-model custom endpoints","url":null,"state":"closed"},{"kind":"pr","number":6863,"title":"fix: always show model selection for multi-model custom providers","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#7187","number":7187}],"category":"merged_via","implication":"superseded"},"merged":false},{"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":7187,"title":"fix(model): salvage model/provider resolution PRs #6860 #6844 #6863 #6943","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/auxiliary_client.py","agent/error_classifier.py","cli.py","hermes_cli/main.py","hermes_cli/model_normalize.py","hermes_cli/model_switch.py","run_agent.py","tests/agent/test_auxiliary_named_custom_providers.py","tests/agent/test_error_classifier.py","tests/hermes_cli/test_codex_models.py","tests/hermes_cli/test_custom_provider_model_switch.py","tests/hermes_cli/test_model_normalize.py","tests/hermes_cli/test_opencode_go_in_model_list.py","tests/run_agent/test_fallback_model.py","tests/run_agent/test_run_agent.py","tests/run_agent/test_switch_model_context.py"]},{"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"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}