{"repo":"NousResearch/hermes-agent","public_id":"13971","display_label":"Ollama/GLM backend detection false positive fix","summary":"A heuristic for identifying Ollama/GLM backends was triggering incorrectly for non-Ollama API proxies and gateways, causing spurious truncation continuation behavior. Fixes narrow the detection logic to explicit Ollama indicators only.","subsystem":"agent","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":53574},"resolution":{"state":"partially_resolved","by":"pr-53574"},"freshness":{"state":"verified","last_verified_at":"2026-06-27T11:03:08+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.43599999999999994},"fix_edges":[{"issue_number":13971,"pr_number":53574,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":15751,"pr_number":53574,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":13971,"title":"[Bug]: `_is_ollama_glm_backend()` false positive causes spurious truncation continuation with local API proxies","url":null,"state":"closed"},{"kind":"issue","number":15751,"title":"GLM models accessed via API gateways are incorrectly treated as Ollama backends","url":null,"state":"closed"},{"kind":"pr","number":14789,"title":"fix(agent): restrict _is_ollama_glm_backend to explicit Ollama indicators","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#53574","number":53574},{"kind":"pr","value":"#13971","number":13971}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":15752,"title":"fix: GLM models via API gateways incorrectly treated as Ollama backends","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#53574","number":53574},{"kind":"pr","value":"#13971","number":13971}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":53574,"title":"fix(agent): restrict Ollama/GLM stop-to-length heuristic to explicit Ollama backends (#13971)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["run_agent.py","tests/run_agent/test_run_agent.py"],"fix_hunks":[{"file":"run_agent.py","hunks":["@@ -1367,14 +1367,26 @@ def _has_natural_response_ending(content: str) -> bool:\n         return False\n \n     def _is_ollama_glm_backend(self) -> bool:\n-        \"\"\"Detect the narrow backend family affected by Ollama/GLM stop misreports.\"\"\"\n+        \"\"\"Detect Ollama-hosted GLM models affected by stop misreports.\n+\n+        Ollama can misreport truncated output as finish_reason='stop'.\n+        Detection relies on explicit Ollama signatures:\n+        - Port 11434 (Ollama default)\n+        - \"ollama\" in the base URL (e.g. ollama.local, /ollama/ path)\n+        - provider explicitly set to \"ollama\"\n+\n+        Crucially it does NOT match arbitrary local/private endpoints\n+        (LiteLLM/sglang/vLLM/LM Studio proxies, Tailscale boxes), which\n+        report finish_reason correctly and were the source of #13971's\n+        false-positive truncation continuations.\n+        \"\"\"\n         model_lower = (self.model or \"\").lower()\n         provider_lower = (self.provider or \"\").lower()\n         if \"glm\" not in model_lower and provider_lower != \"zai\":\n             return False\n         if \"ollama\" in self._base_url_lower or \":11434\" in self._base_url_lower:\n             return True\n-        return bool(self.base_url and is_local_endpoint(self.base_url))\n+        return provider_lower == \"ollama\"\n \n     def _should_treat_stop_as_truncated(\n         self,"]},{"file":"tests/run_agent/test_run_agent.py","hunks":["@@ -4526,6 +4526,159 @@ def test_non_ollama_stop_without_terminal_boundary_does_not_continue(self, agent\n         assert result[\"api_calls\"] == 2\n         assert result[\"final_response\"] == \"Based on the search results, the best next\"\n \n+    def test_sglang_glm_stop_without_terminal_boundary_does_not_continue(self, agent):\n+        \"\"\"sglang/vLLM-hosted GLM models report finish_reason correctly.\n+\n+        The stop->length workaround must NOT apply to non-Ollama local\n+        servers that expose OpenAI-compatible /v1 endpoints (sglang, vLLM,\n+        LM Studio, etc.).  A Chinese-text response ending without ASCII\n+        punctuation should not be reclassified as truncated.\n+        \"\"\"\n+        self._setup_agent(agent)\n+        agent.base_url = \"http://127.0.0.1:60000/v1\"\n+        agent._base_url_lower = agent.base_url.lower()\n+        agent.model = \"glm-5-fp8\"\n+\n+        tool_turn = _mock_response(\n+            content=\"\",\n+            finish_reason=\"tool_calls\",\n+            tool_calls=[_mock_tool_call(name=\"web_search\", arguments=\"{}\", call_id=\"c1\")],\n+        )\n+        # Response ends with Chinese character (no ASCII punctuation) — NOT truncated\n+        normal_stop = _mock_response(\n+            content=\"根据搜索结果，建议修改配置\",\n+            finish_reason=\"stop\",\n+        )\n+        agent.client.chat.completions.create.side_effect = [tool_turn, normal_stop]\n+\n+        with (\n+            patch(\"run_agent.handle_function_call\", return_value=\"search result\"),\n+            patch.object(agent, \"_persist_session\"),\n+            patch.object(agent, \"_save_trajectory\"),\n+            patch.object(agent, \"_cleanup_task_resources\"),\n+        ):\n+            result = agent.run_conversation(\"hello\")\n+\n+        assert result[\"completed\"] is True\n+        assert result[\"api_calls\"] == 2\n+        assert result[\"final_response\"] == \"根据搜索结果，建议修改配置\"\n+\n+    def test_ollama_glm_on_port_11434_still_triggers_heuristic(self, agent):\n+        \"\"\"Ollama on port 11434 should still trigger the stop->length heuristic.\"\"\"\n+        self._setup_agent(agent)\n+        agent.base_url = \"http://localhost:11434/v1\"\n+        agent._base_url_lower = agent.base_url.lower()\n+        agent.model = \"glm-5.1:cloud\"\n+\n+        tool_turn = _mock_response(\n+            content=\"\",\n+            finish_reason=\"tool_calls\",\n+            tool_calls=[_mock_tool_call(name=\"web_search\", arguments=\"{}\", call_id=\"c1\")],\n+        )\n+        misreported_stop = _mock_response(\n+            content=\"Based on the search results, the best next\",\n+            finish_reason=\"stop\",\n+        )\n+        continued = _mock_response(\n+            content=\" step is to update the config.\",\n+            finish_reason=\"stop\",\n+        )\n+        agent.client.chat.completions.create.side_effect = [\n+            tool_turn,\n+            misreported_stop,\n+            continued,\n+        ]\n+\n+        with (\n+            patch(\"run_agent.handle_function_call\", return_value=\"search result\"),\n+            patch.object(agent, \"_persist_session\"),\n+            patch.object(agent, \"_save_trajectory\"),\n+            patch.object(agent, \"_cleanup_task_resources\"),\n+        ):\n+            result = agent.run_conversation(\"hello\")\n+\n+        assert result[\"completed\"] is True\n+        assert result[\"api_calls\"] == 3\n+        third_call_messages = agent.client.chat.completions.create.call_args_list[2].kwargs[\"messages\"]\n+        assert \"truncated by the output length limit\" in third_call_messages[-1][\"content\"]\n+\n+    def test_ollama_provider_without_url_signature_still_triggers_heuristic(self, agent):\n+        \"\"\"provider='ollama' triggers the heuristic even when the base URL\n+        carries no ``ollama``/``:11434`` signature (e.g. a reverse proxy).\"\"\"\n+        self._setup_agent(agent)\n+        agent.base_url = \"http://my-proxy.internal:9000/v1\"\n+        agent._base_url_lower = agent.base_url.lower()\n+        agent.provider = \"ollama\"\n+        agent.model = \"glm-5.1:cloud\"\n+\n+        tool_turn = _mock_response(\n+            content=\"\",\n+            finish_reason=\"tool_calls\",\n+            tool_calls=[_mock_tool_call(name=\"web_search\", arguments=\"{}\", call_id=\"c1\")],\n+        )\n+        misreported_stop = _mock_response(\n+            content=\"Based on the search results, the best next\",\n+            finish_reason=\"stop\",\n+        )\n+        continued = _mock_response(\n+            content=\" step is to update the config.\",\n+            finish_reason=\"stop\",\n+        )\n+        agent.client.chat.completions.create.side_effect = [\n+            tool_turn,\n+            misreported_stop,\n+            continued,\n+        ]\n+\n+        with (\n+            patch(\"run_agent.handle_function_call\", return_value=\"search result\"),\n+            patch.object(agent, \"_persist_session\"),\n+            patch.object(agent, \"_save_trajectory\"),\n+            patch.object(agent, \"_cleanup_task_resources\"),\n+        ):\n+            result = agent.run_conversation(\"hello\")\n+\n+        assert result[\"completed\"] is True\n+        assert result[\"api_calls\"] == 3\n+        third_call_messages = agent.client.chat.completions.create.call_args_list[2].kwargs[\"messages\"]\n+        assert \"truncated by the output length limit\" in third_call_messages[-1][\"content\"]\n+\n+    def test_zai_via_local_proxy_does_not_trigger_heuristic(self, agent):\n+        \"\"\"Issue #13971: a local LiteLLM proxy forwarding to remote Z.AI\n+        must NOT be treated as an Ollama backend. provider='zai' on\n+        localhost:8000 with no ollama/:11434 signature reports stop\n+        correctly and the response should be delivered as-is.\"\"\"\n+        self._setup_agent(agent)\n+        agent.base_url = \"http://localhost:8000/v1\"\n+        agent._base_url_lower = agent.base_url.lower()\n+        agent.provider = \"zai\"\n+        agent.model = \"glm-5-turbo\"\n+\n+        tool_turn = _mock_response(\n+            content=\"\",\n+            finish_reason=\"tool_calls\",\n+            tool_calls=[_mock_tool_call(name=\"web_search\", arguments=\"{}\", call_id=\"c1\")],\n+        )\n+        # Complete response ending without ASCII punctuation — must NOT be\n+        # reclassified as truncated.\n+        normal_stop = _mock_response(\n+            content=\"Done — the config has been updated\",\n+            finish_reason=\"stop\",\n+        )\n+        agent.client.chat.completions.create.side_effect = [tool_turn, normal_stop]\n+\n+        with (\n+            patch(\"run_agent.handle_function_call\", return_value=\"search result\"),\n+            patch.object(agent, \"_persist_session\"),\n+            patch.object(agent, \"_save_trajectory\"),\n+            patch.object(agent, \"_cleanup_task_resources\"),\n+        ):\n+            result = agent.run_conversation(\"hello\")\n+\n+        assert result[\"completed\"] is True\n+        assert result[\"api_calls\"] == 2\n+        assert result[\"final_response\"] == \"Done — the config has been updated\"\n+\n     def test_length_thinking_exhausted_skips_continuation(self, agent):\n         \"\"\"When finish_reason='length' but content is only thinking, skip retries.\"\"\"\n         self._setup_agent(agent)\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}