{"repo":"NousResearch/hermes-agent","public_id":"11017","display_label":"List-format provider models silently dropped or mishandled","summary":"These issues and fixes address a bug where list-format model entries in custom provider configurations were being silently dropped or mishandled, causing problems in the model picker. Multiple fix attempts target normalization and enumeration of dict-format and list-format models.","subsystem":null,"counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":2,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":14495,"merged_at":"2026-04-23T09:37:07+00:00"},"resolution":{"state":"resolved","by":"pr-14495"},"freshness":{"state":"stale","last_verified_at":"2026-04-23T09:37:07+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":8216,"pr_number":12557,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-04-19T18:07:30+00:00"},{"issue_number":8216,"pr_number":14495,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-04-23T09:37:07+00:00"},{"issue_number":8758,"pr_number":12557,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-04-19T18:07:30+00:00"},{"issue_number":9148,"pr_number":12557,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-19T18:07:30+00:00"},{"issue_number":11017,"pr_number":12557,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-04-19T18:07:30+00:00"},{"issue_number":11017,"pr_number":14495,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-23T09:37:07+00:00"},{"issue_number":11499,"pr_number":12557,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-04-19T18:07:30+00:00"},{"issue_number":11677,"pr_number":12557,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-19T18:07:30+00:00"}],"members":[{"kind":"issue","number":11017,"title":"Bug: providers: dict models list silently dropped, causes duplicate entry with 0 models in /model picker","url":null,"state":"closed"},{"kind":"pr","number":11130,"title":"fix #11017: handle list-type models in providers dict format","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#12557","number":12557}],"category":"superseded_by_pr","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"},"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"},"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     \"127238744+teknium1@users.noreply.github.com\": \"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"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}