{"repo":"NousResearch/hermes-agent","public_id":"6211","display_label":"Model name normalization and provider prefix handling","summary":"These issues and fixes concern how model names and provider prefixes are parsed, stripped, and forwarded across CLI commands, config repair, and API requests. Recurring themes include vendor/provider prefixes not being stripped, model fields not propagating correctly to API calls, and inconsistencies between model.name, model.default, and model.model config fields.","subsystem":"agent","counts":{"members":27,"open_issues":3,"open_issues_distinct":3,"open_prs":4,"closed_issues":5,"merged_prs":5,"attempt_prs":14},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":7187},"resolution":{"state":"partially_resolved","by":"pr-7935,pr-54093,pr-115756"},"freshness":{"state":"verified","last_verified_at":"2026-09-19T16:34:41+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.06233618233618232},"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":7828,"pr_number":7935,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":34500,"pr_number":54093,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":64787,"pr_number":115756,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":68347,"pr_number":71144,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":6211,"title":"Z.AI provider prefix not stripped from model name, causing HTTP 400","url":null,"state":"closed"},{"kind":"issue","number":7828,"title":"[Bug]: custom_providers model field not preserved, causing \"Model Not Exist\" errors","url":null,"state":"closed"},{"kind":"issue","number":7922,"title":"Gateway /model command sends provider-prefixed slug as raw model name for non-curated models (HTTP 400 Unknown Model)","url":null,"state":"open"},{"kind":"issue","number":34500,"title":"custom_providers: model name not forwarded to API request","url":null,"state":"closed"},{"kind":"issue","number":55153,"title":"fix: model discovery fails for custom:<name> providers (e.g. custom:fireworks)","url":null,"state":"open"},{"kind":"issue","number":64787,"title":"openai-codex:<model> colon prefix not stripped in normalize_model_for_provider -> HTTP 400","url":null,"state":"closed"},{"kind":"issue","number":67106,"title":"doctor --fix leaves native-provider model IDs vendor-prefixed","url":null,"state":"open"},{"kind":"issue","number":68347,"title":"custom_providers ignores model id field when selecting from model picker; sends prefixed provider/model string instead","url":null,"state":"closed"},{"kind":"pr","number":6224,"title":"fix: strip provider prefix from model name for direct API providers","url":null,"state":"closed","close_reason":{"category":"silent_close","implication":"active"},"merged":false},{"kind":"pr","number":6943,"title":"fix(model): normalize native provider-prefixed model ids","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#7187","number":7187}],"category":"merged_via","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":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","implication":"superseded"},"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\": api_key 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\": \"test-key\",\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\": \"test-key\",\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\": \"test-key\",\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\": \"pool-key\",\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\": \"test-key\",\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"]}]},{"kind":"pr","number":8059,"title":"fix: strip provider prefix from model name in /model command","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#6211","number":6211}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":34502,"title":"fix(cli): treat model.name as alias for model.default","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#54093","number":54093}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":34527,"title":"fix: honor model.name for configured providers","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#54093","number":54093}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":54093,"title":"fix(config): canonicalize model.name/model.model to model.default (#34500)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["gateway/run.py","hermes_cli/config.py","infographic/model-name-canon/infographic.png","tests/cli/test_cli_init.py"]},{"kind":"pr","number":55162,"title":"fix(cli): strip custom: prefix in provider_model_ids for catalog lookups","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"c97d9a4c07b27c8ce94c10e83b916eea43a1e257","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":64788,"title":"fix(codex): strip `openai-codex:` colon prefix in model normalization","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"75400793448e","number":0},{"kind":"pr","value":"#115756","number":115756},{"kind":"pr","value":"#64787","number":64787},{"kind":"pr","value":"#64788","number":64788}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":64916,"title":"fix(openai-codex): strip colon provider prefix in normalize_model_for_provider (#64787)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#64788","number":64788},{"kind":"pr","value":"#64788","number":64788}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":67112,"title":"fix(doctor): normalize model IDs during config repair","url":null,"state":"open","close_reason":{"refs":[{"kind":"pr","value":"#67106","number":67106}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":67169,"title":"fix(doctor): strip vendor prefixes from native-provider model IDs on config migration","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"5b06dbf7626f","number":0},{"kind":"pr","value":"#67106","number":67106},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67173","number":67173},{"kind":"pr","value":"#67169","number":67169},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67169","number":67169},{"kind":"pr","value":"#67173","number":67173},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67173","number":67173},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67173","number":67173},{"kind":"pr","value":"#67173","number":67173},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67173","number":67173},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67169","number":67169},{"kind":"pr","value":"#67106","number":67106}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":67173,"title":"fix(doctor): normalize model IDs during config repair","url":null,"state":"open","close_reason":{"refs":[{"kind":"commit","value":"34344e08af80","number":0},{"kind":"pr","value":"#67106","number":67106},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67173","number":67173},{"kind":"pr","value":"#67169","number":67169},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67169","number":67169},{"kind":"pr","value":"#67173","number":67173},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67173","number":67173},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67173","number":67173},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67173","number":67173},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67169","number":67169},{"kind":"pr","value":"#67106","number":67106},{"kind":"pr","value":"#374151","number":374151},{"kind":"pr","value":"#67106","number":67106},{"kind":"pr","value":"#67112","number":67112},{"kind":"pr","value":"#67173","number":67173}],"category":"partial_fix","implication":"needs_llm"},"merged":false},{"kind":"pr","number":68436,"title":"fix(models): resolve custom provider model ids","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#71144","number":71144}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":71144,"title":"fix(model_switch): don't send a picker prefix as the custom provider model id","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["contributors/emails/[redacted:email]","hermes_cli/model_switch.py","tests/hermes_cli/test_model_switch_custom_providers.py"]},{"kind":"pr","number":71558,"title":"fix(cli): centralize matching provider: and provider/ prefix stripping","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"75400793448e","number":0},{"kind":"pr","value":"#115756","number":115756},{"kind":"pr","value":"#64787","number":64787},{"kind":"pr","value":"#64788","number":64788}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":115756,"title":"fix(cli): `-m openai-codex:<model>` and other provider:model switches no longer send the prefix to the API (#64787, salvage #64788)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["contributors/emails/[redacted:email]","hermes_cli/model_normalize.py","tests/hermes_cli/test_model_normalize.py"]}],"trust":{"default_tier":"verified","includes_suggested":false}}