{"repo":"NousResearch/hermes-agent","public_id":"16970","display_label":"Model picker filtering and deduplication fixes","summary":"These issues and PRs address bugs in the /model picker related to honoring model_catalog.enabled=false settings and deduplicating rows when custom provider endpoints match built-in models.","subsystem":null,"counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":17511,"merged_at":"2026-04-29T15:11:06+00:00"},"resolution":{"state":"resolved","by":"pr-17511"},"freshness":{"state":"verified","last_verified_at":"2026-04-29T15:11:06+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":16970,"pr_number":17511,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-29T15:11:06+00:00"}],"members":[{"kind":"issue","number":16970,"title":"Bug: Model Picker ignores model_catalog.enabled=false","url":null,"state":"closed"},{"kind":"pr","number":16975,"title":"fix(cli): respect model_catalog filtering in /model picker","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#17511","number":17511}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":17218,"title":"fix(model_switch): honor model_catalog.enabled=false in /model picker (#16970)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#17511","number":17511}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":17511,"title":"fix(model_switch): dedup /model picker rows when custom provider endpoint matches a built-in (#16970)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/model_switch.py","tests/hermes_cli/test_user_providers_model_switch.py"],"fix_hunks":[{"file":"hermes_cli/model_switch.py","hunks":["@@ -1018,6 +1018,37 @@ def list_authenticated_providers(\n     results: List[dict] = []\n     seen_slugs: set = set()  # lowercase-normalized to catch case variants (#9545)\n     seen_mdev_ids: set = set()  # prevent duplicate entries for aliases (e.g. kimi-coding + kimi-coding-cn)\n+    # Effective base URLs of every built-in row we emit (normalized lower+rstrip).\n+    # Section 4 uses this to hide ``custom_providers`` entries that point at the\n+    # same endpoint as a built-in (e.g. a user-defined \"my-dashscope\" on\n+    # https://coding-intl.dashscope.aliyuncs.com/v1 collides with the built-in\n+    # alibaba-coding-plan row when DASHSCOPE_API_KEY is present). Fixes #16970.\n+    _builtin_endpoints: set = set()\n+\n+    def _norm_url(url: str) -> str:\n+        return str(url or \"\").strip().rstrip(\"/\").lower()\n+\n+    def _record_builtin_endpoint(slug: str) -> None:\n+        \"\"\"Record the effective base URL for a built-in provider row.\n+\n+        Prefers the live env-override (e.g. DASHSCOPE_BASE_URL) over the\n+        static inference_base_url so the dedup matches what a user typing\n+        that URL into custom_providers would actually hit.\"\"\"\n+        try:\n+            from hermes_cli.auth import PROVIDER_REGISTRY as _reg\n+        except Exception:\n+            return\n+        pcfg = _reg.get(slug)\n+        if not pcfg:\n+            return\n+        url = \"\"\n+        if getattr(pcfg, \"base_url_env_var\", \"\"):\n+            url = os.environ.get(pcfg.base_url_env_var, \"\") or \"\"\n+        if not url:\n+            url = getattr(pcfg, \"inference_base_url\", \"\") or \"\"\n+        normed = _norm_url(url)\n+        if normed:\n+            _builtin_endpoints.add(normed)\n \n     data = fetch_models_dev()\n ","@@ -1124,6 +1155,7 @@ def list_authenticated_providers(\n         })\n         seen_slugs.add(slug.lower())\n         seen_mdev_ids.add(mdev_id)\n+        _record_builtin_endpoint(slug)\n \n     # --- 2. Check Hermes-only providers (nous, openai-codex, copilot, opencode-go) ---\n     from hermes_cli.providers import HERMES_OVERLAYS","@@ -1238,6 +1270,7 @@ def list_authenticated_providers(\n         })\n         seen_slugs.add(pid.lower())\n         seen_slugs.add(hermes_slug.lower())\n+        _record_builtin_endpoint(hermes_slug)\n \n     # --- 2b. Cross-check canonical provider list ---\n     # Catches providers that are in CANONICAL_PROVIDERS but weren't found","@@ -1317,6 +1350,7 @@ def list_authenticated_providers(\n             \"source\": \"canonical\",\n         })\n         seen_slugs.add(_cp.slug.lower())\n+        _record_builtin_endpoint(_cp.slug)\n \n     # --- 3. User-defined endpoints from config ---\n     # Track (name, base_url) of what section 3 emits so section 4 can skip","@@ -1526,6 +1560,15 @@ def list_authenticated_providers(\n             )\n             if _pair_key[0] and _pair_key[1] and _pair_key in _section3_emitted_pairs:\n                 continue\n+            # Skip if a built-in row (sections 1/2/2b) already represents this\n+            # endpoint. Fixes #16970: a user-defined \"my-dashscope\" pointing at\n+            # https://coding-intl.dashscope.aliyuncs.com/v1 duplicates the\n+            # built-in alibaba-coding-plan row whenever DASHSCOPE_API_KEY is\n+            # set. The built-in row carries the curated model list, correct\n+            # auth wiring, and canonical slug — keep it and hide the shadow.\n+            _grp_url_norm = _pair_key[1]\n+            if _grp_url_norm and _grp_url_norm in _builtin_endpoints:\n+                continue\n             results.append({\n                 \"slug\": slug,\n                 \"name\": grp[\"name\"],"]},{"file":"tests/hermes_cli/test_user_providers_model_switch.py","hunks":["@@ -453,6 +453,142 @@ def test_list_authenticated_providers_no_duplicate_labels_across_schemas(monkeyp\n     )\n \n \n+def test_list_authenticated_providers_hides_custom_shadowing_builtin_endpoint(monkeypatch):\n+    \"\"\"#16970: a custom_providers entry whose ``base_url`` matches a built-in\n+    provider's endpoint should be hidden. The built-in row already represents\n+    that endpoint with its canonical slug, curated model list, and auth wiring.\n+\n+    Repro: user sets ``DASHSCOPE_API_KEY`` (triggers the built-in ``alibaba``\n+    row pointing at the static ``inference_base_url``) AND defines a\n+    ``my-alibaba`` custom provider pointing at the same URL. Before the fix,\n+    the picker showed both rows for one endpoint.\n+    \"\"\"\n+    monkeypatch.setenv(\"DASHSCOPE_API_KEY\", \"sk-test\")\n+    monkeypatch.setattr(\n+        \"agent.models_dev.fetch_models_dev\",\n+        lambda: {\n+            \"alibaba\": {\n+                \"name\": \"Alibaba Cloud (DashScope)\",\n+                \"env\": [\"DASHSCOPE_API_KEY\"],\n+            }\n+        },\n+    )\n+    monkeypatch.setattr(\"hermes_cli.providers.HERMES_OVERLAYS\", {})\n+\n+    custom_providers = [\n+        {\n+            \"name\": \"my-alibaba\",\n+            # Matches PROVIDER_REGISTRY['alibaba'].inference_base_url exactly.\n+            \"base_url\": \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\",\n+            \"api_key\": \"[redacted:secret-assignment]\",\n+            \"model\": \"qwen3.6-plus\",\n+            \"models\": {\"qwen3.6-plus\": {\"context_length\": 500000}},\n+        }\n+    ]\n+\n+    providers = list_authenticated_providers(\n+        current_provider=\"my-alibaba\",\n+        user_providers={},\n+        custom_providers=custom_providers,\n+        max_models=50,\n+    )\n+\n+    slugs = [p[\"slug\"] for p in providers]\n+    # Built-in alibaba row should be present.\n+    assert \"alibaba\" in slugs, (\n+        f\"Expected built-in alibaba row, got slugs: {slugs}\"\n+    )\n+    # Custom shadow row should be hidden — its base_url matches the built-in's.\n+    assert not any(\"my-alibaba\" in s for s in slugs), (\n+        f\"Custom my-alibaba should have been dedup'd against the built-in \"\n+        f\"alibaba endpoint, got slugs: {slugs}\"\n+    )\n+\n+\n+def test_list_authenticated_providers_keeps_custom_with_distinct_endpoint(monkeypatch):\n+    \"\"\"Dedup must only apply when the endpoint matches a built-in. A custom\n+    provider on a genuinely distinct endpoint stays visible even if a\n+    built-in is also authenticated.\"\"\"\n+    monkeypatch.setenv(\"DASHSCOPE_API_KEY\", \"sk-test\")\n+    monkeypatch.setattr(\n+        \"agent.models_dev.fetch_models_dev\",\n+        lambda: {\n+            \"alibaba\": {\n+                \"name\": \"Alibaba Cloud (DashScope)\",\n+                \"env\": [\"DASHSCOPE_API_KEY\"],\n+            }\n+        },\n+    )\n+    monkeypatch.setattr(\"hermes_cli.providers.HERMES_OVERLAYS\", {})\n+\n+    custom_providers = [\n+        {\n+            \"name\": \"my-private-relay\",\n+            \"base_url\": \"https://relay.example.internal/v1\",\n+            \"api_key\": \"[redacted:secret-assignment]\",\n+            \"model\": \"qwen3.6-plus\",\n+            \"models\": {\"qwen3.6-plus\": {}},\n+        }\n+    ]\n+\n+    providers = list_authenticated_providers(\n+        current_provider=\"my-private-relay\",\n+        user_providers={},\n+        custom_providers=custom_providers,\n+        max_models=50,\n+    )\n+\n+    slugs = [p[\"slug\"] for p in providers]\n+    assert any(\"my-private-relay\" in s for s in slugs), (\n+        f\"Custom provider on distinct endpoint must stay visible, got: {slugs}\"\n+    )\n+\n+\n+def test_list_authenticated_providers_dedup_honors_base_url_env_override(monkeypatch):\n+    \"\"\"The dedup must track the EFFECTIVE endpoint — if DASHSCOPE_BASE_URL\n+    overrides the static inference_base_url, a custom provider pointing at\n+    the overridden URL (not the static one) should still be recognized as\n+    a duplicate.\"\"\"\n+    monkeypatch.setenv(\"DASHSCOPE_API_KEY\", \"sk-test\")\n+    monkeypatch.setenv(\n+        \"DASHSCOPE_BASE_URL\",\n+        \"https://custom-dashscope.example.com/v1\",\n+    )\n+    monkeypatch.setattr(\n+        \"agent.models_dev.fetch_models_dev\",\n+        lambda: {\n+            \"alibaba\": {\n+                \"name\": \"Alibaba Cloud (DashScope)\",\n+                \"env\": [\"DASHSCOPE_API_KEY\"],\n+            }\n+        },\n+    )\n+    monkeypatch.setattr(\"hermes_cli.providers.HERMES_OVERLAYS\", {})\n+\n+    custom_providers = [\n+        {\n+            \"name\": \"my-dashscope-override\",\n+            # Same URL as DASHSCOPE_BASE_URL env override above.\n+            \"base_url\": \"https://custom-dashscope.example.com/v1\",\n+            \"api_key\": \"[redacted:secret-assignment]\",\n+            \"model\": \"qwen3.6-plus\",\n+        }\n+    ]\n+\n+    providers = list_authenticated_providers(\n+        current_provider=\"alibaba\",\n+        user_providers={},\n+        custom_providers=custom_providers,\n+        max_models=50,\n+    )\n+\n+    slugs = [p[\"slug\"] for p in providers]\n+    assert not any(\"my-dashscope-override\" in s for s in slugs), (\n+        f\"Custom entry matching env-overridden built-in endpoint should be \"\n+        f\"dedup'd, got: {slugs}\"\n+    )\n+\n+\n # =============================================================================\n # Tests for _get_named_custom_provider with providers: dict\n # =============================================================================\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}