{"repo":"NousResearch/hermes-agent","public_id":"7828","display_label":"Custom providers model field not propagated from config","summary":"One bug report describes the custom_providers model field not being preserved, leading to \"Model Not Exist\" errors. Two pull requests with identical titles propose propagating the model field from config so the API receives the correct model name.","subsystem":null,"counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":7935,"merged_at":"2026-04-11T21:09:40+00:00"},"resolution":{"state":"resolved","by":"pr-7935"},"freshness":{"state":"stale","last_verified_at":"2026-04-11T21:09:40+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":7828,"pr_number":7935,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-04-11T21:09:40+00:00"}],"members":[{"kind":"issue","number":7828,"title":"[Bug]: custom_providers model field not preserved, causing \"Model Not Exist\" errors","url":null,"state":"closed"},{"kind":"pr","number":7916,"title":"fix(custom-providers): propagate model field from config so API receives the correct model name","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#7935","number":7935}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":7935,"title":"fix(custom-providers): propagate model field from config so API receives the correct model name","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["cli.py","hermes_cli/config.py","hermes_cli/runtime_provider.py","tests/hermes_cli/test_runtime_provider_resolution.py"],"fix_hunks":[{"file":"cli.py","hunks":["@@ -2710,6 +2710,15 @@ def _ensure_runtime_credentials(self) -> bool:\n         self.api_key = [redacted:secret-assignment]\n         self.base_url = base_url\n \n+        # When a custom_provider entry carries an explicit `model` field,\n+        # use it as the effective model name.  Without this, running\n+        # `hermes chat --model <provider-name>` sends the provider name\n+        # (e.g. \"my-provider\") as the model string to the API instead of\n+        # the configured model (e.g. \"qwen3.6-plus\"), causing 400 errors.\n+        runtime_model = runtime.get(\"model\")\n+        if runtime_model and isinstance(runtime_model, str):\n+            self.model = runtime_model\n+\n         # Normalize model for the resolved provider (e.g. swap non-Codex\n         # models when provider is openai-codex).  Fixes #651.\n         model_changed = self._normalize_model_for_provider(resolved_provider)"]},{"file":"hermes_cli/config.py","hunks":["@@ -1497,7 +1497,7 @@ def check_config_version() -> Tuple[int, int]:\n \n # Valid fields inside a custom_providers list entry\n _VALID_CUSTOM_PROVIDER_FIELDS = {\n-    \"name\", \"base_url\", \"api_key\", \"api_mode\", \"models\",\n+    \"name\", \"base_url\", \"api_key\", \"api_mode\", \"model\", \"models\",\n     \"context_length\", \"rate_limit_delay\",\n }\n "]},{"file":"hermes_cli/runtime_provider.py","hunks":["@@ -304,6 +304,9 @@ def _get_named_custom_provider(requested_provider: str) -> Optional[Dict[str, An\n         api_mode = _parse_api_mode(entry.get(\"api_mode\"))\n         if api_mode:\n             result[\"api_mode\"] = api_mode\n+        model_name = str(entry.get(\"model\", \"\") or \"\").strip()\n+        if model_name:\n+            result[\"model\"] = model_name\n         return result\n \n     return None","@@ -329,6 +332,11 @@ def _resolve_named_custom_runtime(\n     # Check if a credential pool exists for this custom endpoint\n     pool_result = _try_resolve_from_custom_pool(base_url, \"custom\", custom_provider.get(\"api_mode\"))\n     if pool_result:\n+        # Propagate the model name even when using pooled credentials —\n+        # the pool doesn't know about the custom_providers model field.\n+        model_name = custom_provider.get(\"model\")\n+        if model_name:\n+            pool_result[\"model\"] = model_name\n         return pool_result\n \n     api_key_candidates = [","@@ -339,7 +347,7 @@ def _resolve_named_custom_runtime(\n     ]\n     api_key = [redacted:secret-assignment] for candidate in api_key_candidates if has_usable_secret(candidate)), \"\")\n \n-    return {\n+    result = {\n         \"provider\": \"custom\",\n         \"api_mode\": custom_provider.get(\"api_mode\")\n         or _detect_api_mode_for_url(base_url)","@@ -348,6 +356,11 @@ def _resolve_named_custom_runtime(\n         \"api_key\": [redacted:secret-assignment] or \"no-key-required\",\n         \"source\": f\"custom_provider:{custom_provider.get('name', requested_provider)}\",\n     }\n+    # Propagate the model name so callers can override self.model when the\n+    # provider name differs from the actual model string the API expects.\n+    if custom_provider.get(\"model\"):\n+        result[\"model\"] = custom_provider[\"model\"]\n+    return result\n \n \n def _resolve_openrouter_runtime("]},{"file":"tests/hermes_cli/test_runtime_provider_resolution.py","hunks":["@@ -1214,3 +1214,115 @@ def test_openrouter_provider_not_affected_by_custom_fix(monkeypatch):\n \n     resolved = rp.resolve_runtime_provider(requested=\"openrouter\")\n     assert resolved[\"provider\"] == \"openrouter\"\n+\n+\n+# ------------------------------------------------------------------\n+# fix #7828 — custom_providers model field must propagate to runtime\n+# ------------------------------------------------------------------\n+\n+\n+def test_get_named_custom_provider_includes_model(monkeypatch):\n+    \"\"\"_get_named_custom_provider should include the model field from config.\"\"\"\n+    monkeypatch.setattr(rp, \"load_config\", lambda: {\n+        \"custom_providers\": [{\n+            \"name\": \"my-dashscope\",\n+            \"base_url\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n+            \"api_key\": \"[redacted:secret-assignment]\",\n+            \"api_mode\": \"chat_completions\",\n+            \"model\": \"qwen3.6-plus\",\n+        }],\n+    })\n+\n+    result = rp._get_named_custom_provider(\"my-dashscope\")\n+    assert result is not None\n+    assert result[\"model\"] == \"qwen3.6-plus\"\n+\n+\n+def test_get_named_custom_provider_excludes_empty_model(monkeypatch):\n+    \"\"\"Empty or whitespace-only model field should not appear in result.\"\"\"\n+    for model_val in [\"\", \"   \", None]:\n+        entry = {\n+            \"name\": \"test-ep\",\n+            \"base_url\": \"https://example.com/v1\",\n+            \"api_key\": \"key\",\n+        }\n+        if model_val is not None:\n+            entry[\"model\"] = model_val\n+\n+        monkeypatch.setattr(rp, \"load_config\", lambda e=entry: {\n+            \"custom_providers\": [e],\n+        })\n+\n+        result = rp._get_named_custom_provider(\"test-ep\")\n+        assert result is not None\n+        assert \"model\" not in result, (\n+            f\"model field {model_val!r} should not be included in result\"\n+        )\n+\n+\n+def test_named_custom_runtime_propagates_model_direct_path(monkeypatch):\n+    \"\"\"Model should propagate through the direct (non-pool) resolution path.\"\"\"\n+    monkeypatch.setattr(rp, \"resolve_provider\", lambda *a, **k: \"my-server\")\n+    monkeypatch.setattr(\n+        rp, \"_get_named_custom_provider\",\n+        lambda p: {\n+            \"name\": \"my-server\",\n+            \"base_url\": \"http://localhost:8000/v1\",\n+            \"api_key\": \"[redacted:secret-assignment]\",\n+            \"model\": \"qwen3.6-plus\",\n+        },\n+    )\n+    # Ensure pool doesn't intercept\n+    monkeypatch.setattr(rp, \"_try_resolve_from_custom_pool\", lambda *a, **k: None)\n+\n+    resolved = rp.resolve_runtime_provider(requested=\"my-server\")\n+    assert resolved[\"model\"] == \"qwen3.6-plus\"\n+    assert resolved[\"provider\"] == \"custom\"\n+\n+\n+def test_named_custom_runtime_propagates_model_pool_path(monkeypatch):\n+    \"\"\"Model should propagate even when credential pool handles credentials.\"\"\"\n+    monkeypatch.setattr(rp, \"resolve_provider\", lambda *a, **k: \"my-server\")\n+    monkeypatch.setattr(\n+        rp, \"_get_named_custom_provider\",\n+        lambda p: {\n+            \"name\": \"my-server\",\n+            \"base_url\": \"http://localhost:8000/v1\",\n+            \"api_key\": \"[redacted:secret-assignment]\",\n+            \"model\": \"qwen3.6-plus\",\n+        },\n+    )\n+    # Pool returns a result (intercepting the normal path)\n+    monkeypatch.setattr(\n+        rp, \"_try_resolve_from_custom_pool\",\n+        lambda *a, **k: {\n+            \"provider\": \"custom\",\n+            \"api_mode\": \"chat_completions\",\n+            \"base_url\": \"http://localhost:8000/v1\",\n+            \"api_key\": \"[redacted:secret-assignment]\",\n+            \"source\": \"pool:custom:my-server\",\n+        },\n+    )\n+\n+    resolved = rp.resolve_runtime_provider(requested=\"my-server\")\n+    assert resolved[\"model\"] == \"qwen3.6-plus\", (\n+        \"model must be injected into pool result\"\n+    )\n+    assert resolved[\"api_key\"] == \"pool-key\", \"pool credentials should be used\"\n+\n+\n+def test_named_custom_runtime_no_model_when_absent(monkeypatch):\n+    \"\"\"When custom_providers entry has no model field, runtime should not either.\"\"\"\n+    monkeypatch.setattr(rp, \"resolve_provider\", lambda *a, **k: \"my-server\")\n+    monkeypatch.setattr(\n+        rp, \"_get_named_custom_provider\",\n+        lambda p: {\n+            \"name\": \"my-server\",\n+            \"base_url\": \"http://localhost:8000/v1\",\n+            \"api_key\": \"[redacted:secret-assignment]\",\n+        },\n+    )\n+    monkeypatch.setattr(rp, \"_try_resolve_from_custom_pool\", lambda *a, **k: None)\n+\n+    resolved = rp.resolve_runtime_provider(requested=\"my-server\")\n+    assert \"model\" not in resolved\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}