{"repo":"NousResearch/hermes-agent","public_id":"12988","display_label":"DeepSeek V4 and Ollama Cloud model ID fixes","summary":"These items cover adding DeepSeek V4 model identifiers and pricing, fixing normalization bugs that silently fold V4 IDs to wrong names or break custom endpoints like Volcengine ARK, and addressing Ollama Cloud issues where :cloud/-cloud suffixes cause invalid model IDs.","subsystem":"agent","counts":{"members":17,"open_issues":3,"open_issues_distinct":3,"open_prs":3,"closed_issues":5,"merged_prs":2,"attempt_prs":7},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":19881},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-04T19:38:16+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.08735294117647058},"fix_edges":[{"issue_number":16179,"pr_number":19881,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":12988,"title":"[How-to] Volcengine Coding Plan configuration + model_normalize bug with vendor-prefixed model IDs","url":null,"state":"open"},{"kind":"issue","number":14902,"title":"[Feature]: support for deepseek-v4-pro model","url":null,"state":"closed"},{"kind":"issue","number":15936,"title":"[特点]Native support for DeepSeek V4 series (deepseek-v4-pro / flash)","url":null,"state":"closed"},{"kind":"issue","number":16160,"title":"deepseek-v4-pro model name gets normalized to deepseek-chat silently","url":null,"state":"closed"},{"kind":"issue","number":16179,"title":"[Bug]: Ollama Cloud model list shows wrong names with :cloud / -cloud suffix (e.g. kimi-k2.6:cloud), causing 400/404","url":null,"state":"closed"},{"kind":"issue","number":17199,"title":"deepseek provider: model normalization and base_url override break custom endpoints (e.g. Volcengine ARK)","url":null,"state":"open"},{"kind":"issue","number":19128,"title":"[Feature]: add `qwen3.6-flash`, `deepseek-v4-flash`, `deepseek-v4-pro` for Alibaba provider","url":null,"state":"closed"},{"kind":"issue","number":45137,"title":"ollama-cloud: validate_requested_model rejects bare model IDs like 'deepseek-v4-flash' because normalize_model_for_provider doesn't strip :cloud/-cloud suffix","url":null,"state":"open"},{"kind":"pr","number":14891,"title":"feat: add DeepSeek V4 model identifiers","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#14946","number":14946},{"kind":"pr","value":"#14934","number":14934}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":14911,"title":"feat(models): add deepseek-v4-pro to DeepSeek provider catalog","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#14891","number":14891}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":15123,"title":"fix(model-normalize): pass DeepSeek V-series IDs through instead of folding to deepseek-chat","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":16192,"title":"fix(models): strip :cloud/-cloud suffix from models.dev Ollama Cloud IDs","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19881","number":19881}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":16196,"title":"fix(cli): implement self-healing sanitization for Ollama Cloud model IDs","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#19881","number":19881},{"kind":"pr","value":"#19881","number":19881}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":17217,"title":"fix: pass through Volcengine ARK endpoint IDs (ep-*) in DeepSeek normalization","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":19529,"title":"feat(models): add Alibaba qwen3.6-flash","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":19881,"title":"fix(models): strip :cloud/-cloud suffix from models.dev Ollama Cloud IDs","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/models.py","tests/hermes_cli/test_ollama_cloud_provider.py"],"fix_hunks":[{"file":"hermes_cli/models.py","hunks":["@@ -2906,6 +2906,19 @@ def fetch_api_models(\n _OLLAMA_CLOUD_CACHE_TTL = 3600  # 1 hour\n \n \n+def _strip_ollama_cloud_suffix(model_id: str) -> str:\n+    \"\"\"Strip :cloud / -cloud suffixes that models.dev appends to Ollama Cloud IDs.\n+\n+    The live API uses clean IDs (e.g. 'kimi-k2.6') while models.dev sometimes\n+    returns them as 'kimi-k2.6:cloud'. Normalising before the dedup merge\n+    prevents duplicate entries in the merged model list.\n+    \"\"\"\n+    for suffix in (\":cloud\", \"-cloud\"):\n+        if model_id.endswith(suffix):\n+            return model_id[: -len(suffix)]\n+    return model_id\n+\n+\n def _ollama_cloud_cache_path() -> Path:\n     \"\"\"Return the path for the Ollama Cloud model cache.\"\"\"\n     from hermes_constants import get_hermes_home","@@ -3001,9 +3014,10 @@ def fetch_ollama_cloud_models(\n                 seen.add(m)\n                 merged.append(m)\n         for m in mdev_models:\n-            if m and m not in seen:\n-                seen.add(m)\n-                merged.append(m)\n+            normalized = _strip_ollama_cloud_suffix(m)\n+            if normalized and normalized not in seen:\n+                seen.add(normalized)\n+                merged.append(normalized)\n         if merged:\n             _save_ollama_cloud_cache(merged)\n             return merged"]},{"file":"tests/hermes_cli/test_ollama_cloud_provider.py","hunks":["@@ -401,6 +401,103 @@ def test_get_provider(self):\n         assert pdef.transport == \"openai_chat\"\n \n \n+# ── Cloud Suffix Stripping ──\n+\n+class TestOllamaCloudSuffixStripping:\n+    \"\"\"models.dev appends :cloud / -cloud suffixes that the live API omits.\n+\n+    fetch_ollama_cloud_models() must normalise these before the dedup merge so\n+    users never see broken IDs like 'kimi-k2.6:cloud' in the model picker.\n+    \"\"\"\n+\n+    def test_strips_colon_cloud_suffix(self, tmp_path, monkeypatch):\n+        \"\"\":cloud suffix from models.dev is stripped before merge.\"\"\"\n+        from hermes_cli.models import fetch_ollama_cloud_models\n+\n+        monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path))\n+        monkeypatch.delenv(\"OLLAMA_API_KEY\", raising=False)\n+\n+        mock_mdev = {\n+            \"ollama-cloud\": {\n+                \"models\": {\"kimi-k2.6:cloud\": {\"tool_call\": True}}\n+            }\n+        }\n+        with patch(\"agent.models_dev.fetch_models_dev\", return_value=mock_mdev):\n+            result = fetch_ollama_cloud_models(force_refresh=True)\n+\n+        assert \"kimi-k2.6\" in result\n+        assert \"kimi-k2.6:cloud\" not in result\n+\n+    def test_strips_dash_cloud_suffix(self, tmp_path, monkeypatch):\n+        \"\"\"-cloud suffix from models.dev is stripped before merge.\"\"\"\n+        from hermes_cli.models import fetch_ollama_cloud_models\n+\n+        monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path))\n+        monkeypatch.delenv(\"OLLAMA_API_KEY\", raising=False)\n+\n+        mock_mdev = {\n+            \"ollama-cloud\": {\n+                \"models\": {\"qwen3-coder:480b-cloud\": {\"tool_call\": True}}\n+            }\n+        }\n+        with patch(\"agent.models_dev.fetch_models_dev\", return_value=mock_mdev):\n+            result = fetch_ollama_cloud_models(force_refresh=True)\n+\n+        assert \"qwen3-coder:480b\" in result\n+        assert \"qwen3-coder:480b-cloud\" not in result\n+\n+    def test_no_duplicate_when_live_clean_and_mdev_suffixed(self, tmp_path, monkeypatch):\n+        \"\"\"Live API returns clean ID; mdev has :cloud variant — result has exactly one entry.\"\"\"\n+        from hermes_cli.models import fetch_ollama_cloud_models\n+\n+        monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path))\n+        monkeypatch.setenv(\"OLLAMA_API_KEY\", \"test-key\")\n+\n+        mock_mdev = {\n+            \"ollama-cloud\": {\n+                \"models\": {\n+                    \"kimi-k2.6:cloud\": {\"tool_call\": True},\n+                    \"glm-5.1:cloud\": {\"tool_call\": True},\n+                }\n+            }\n+        }\n+        with patch(\"hermes_cli.models.fetch_api_models\", return_value=[\"kimi-k2.6\", \"glm-5.1\"]), \\\n+             patch(\"agent.models_dev.fetch_models_dev\", return_value=mock_mdev):\n+            result = fetch_ollama_cloud_models(force_refresh=True)\n+\n+        assert result.count(\"kimi-k2.6\") == 1\n+        assert result.count(\"glm-5.1\") == 1\n+        assert \"kimi-k2.6:cloud\" not in result\n+        assert \"glm-5.1:cloud\" not in result\n+\n+    def test_unsuffixed_model_id_unchanged(self, tmp_path, monkeypatch):\n+        \"\"\"Model IDs without :cloud / -cloud suffix are passed through unchanged.\"\"\"\n+        from hermes_cli.models import fetch_ollama_cloud_models\n+\n+        monkeypatch.setenv(\"HERMES_HOME\", str(tmp_path))\n+        monkeypatch.delenv(\"OLLAMA_API_KEY\", raising=False)\n+\n+        mock_mdev = {\n+            \"ollama-cloud\": {\n+                \"models\": {\"nemotron-3-nano:30b\": {\"tool_call\": True}}\n+            }\n+        }\n+        with patch(\"agent.models_dev.fetch_models_dev\", return_value=mock_mdev):\n+            result = fetch_ollama_cloud_models(force_refresh=True)\n+\n+        assert \"nemotron-3-nano:30b\" in result\n+\n+    def test_strip_suffix_helper(self):\n+        \"\"\"Unit test for the _strip_ollama_cloud_suffix helper.\"\"\"\n+        from hermes_cli.models import _strip_ollama_cloud_suffix\n+\n+        assert _strip_ollama_cloud_suffix(\"kimi-k2.6:cloud\") == \"kimi-k2.6\"\n+        assert _strip_ollama_cloud_suffix(\"glm-5.1:cloud\") == \"glm-5.1\"\n+        assert _strip_ollama_cloud_suffix(\"qwen3-coder:480b-cloud\") == \"qwen3-coder:480b\"\n+        assert _strip_ollama_cloud_suffix(\"nemotron-3-nano:30b\") == \"nemotron-3-nano:30b\"\n+        assert _strip_ollama_cloud_suffix(\"\") == \"\"\n+\n+\n # ── Auxiliary Model ──\n \n class TestOllamaCloudAuxiliary:\n"]}]},{"kind":"pr","number":45163,"title":"fix(models): strip :cloud/-cloud suffix for ollama-cloud model IDs","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}