{"repo":"NousResearch/hermes-agent","public_id":"3846","display_label":"Provider config, auth, and API error fixes","summary":"These issues and PRs cover several adjacent but distinct themes: provider routing bugs (auto+base_url, OpenCode Go/Zen showing wrong models), authentication and credential problems (Nous 401 refresh, Telegram auth loss, OAuth gate 401s), and general API errors. No single unified theme, but most relate to provider configuration or auth failures.","subsystem":"gateway","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":54441,"merged_at":"2026-06-28T22:11:55+00:00"},"resolution":{"state":"resolved","by":"pr-54441"},"freshness":{"state":"verified","last_verified_at":"2026-06-28T22:11:55+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":3846,"pr_number":54441,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-28T22:11:55+00:00"}],"members":[{"kind":"issue","number":3846,"title":"[Bug]: telegram bot responds with random error i dont know how to fix!","url":null,"state":"closed"},{"kind":"pr","number":3997,"title":"fix(provider): auto+base_url bypasses cloud API when custom endpoint configured","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"b0b7ff0d7","number":0},{"kind":"pr","value":"#54441","number":54441},{"kind":"pr","value":"#3846","number":3846}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":54441,"title":"fix(provider): auto+base_url bypasses cloud API when custom endpoint configured (#3846)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/runtime_provider.py","tests/hermes_cli/test_runtime_provider_resolution.py"],"fix_hunks":[{"file":"hermes_cli/runtime_provider.py","hunks":["@@ -1454,6 +1454,43 @@ def resolve_runtime_provider(\n         custom_runtime[\"requested_provider\"] = requested_provider\n         return custom_runtime\n \n+    # If provider is \"auto\" (or unset) but config.yaml has an explicit base_url\n+    # pointing at a custom/local endpoint (e.g. Ollama at localhost:11434),\n+    # route through the OpenAI-compatible resolver instead of letting\n+    # resolve_provider() pick up an ANTHROPIC_API_KEY or OPENAI_API_KEY from\n+    # the environment and send the request to a cloud API. Fixes #3846.\n+    if not explicit_base_url and not explicit_api_key:\n+        model_cfg = _get_model_config()\n+        cfg_provider = str(model_cfg.get(\"provider\") or \"\").strip().lower()\n+        cfg_base_url = str(model_cfg.get(\"base_url\") or \"\").strip()\n+        if cfg_base_url and cfg_provider in (\"auto\", \"\"):\n+            # Check that base_url isn't one of the well-known cloud API roots\n+            # (OpenRouter, Anthropic, OpenAI). If it's something else (Ollama,\n+            # LM Studio, vLLM, …) we honour it directly. The full detection\n+            # logic lives in _resolve_openrouter_runtime; we just skip the\n+            # resolve_provider() call so env-var credentials don't shadow it.\n+            # Match on HOST, not substring, so a look-alike base_url\n+            # (e.g. http://api.anthropic.com.attacker.test/v1, or one whose\n+            # path merely contains \"openai.com\") cannot evade the bypass and\n+            # leak a cloud credential. Mirrors the host-gating used for\n+            # API-key selection in _resolve_openrouter_runtime.\n+            _known_cloud_hosts = (\n+                \"openrouter.ai\",\n+                \"anthropic.com\",\n+                \"openai.com\",\n+            )\n+            if not any(\n+                base_url_host_matches(cfg_base_url, host)\n+                for host in _known_cloud_hosts\n+            ):\n+                runtime = _resolve_openrouter_runtime(\n+                    requested_provider=requested_provider,\n+                    explicit_api_key=[redacted:secret-assignment]\n+                    explicit_base_url=explicit_base_url,\n+                )\n+                runtime[\"requested_provider\"] = requested_provider\n+                return runtime\n+\n     provider = resolve_provider(\n         requested_provider,\n         explicit_api_key=[redacted:secret-assignment]"]},{"file":"tests/hermes_cli/test_runtime_provider_resolution.py","hunks":["@@ -2893,3 +2893,102 @@ def test_resolve_runtime_provider_bedrock_nonclaude_target_model_uses_converse(m\n     assert resolved[\"provider\"] == \"bedrock\"\n     assert resolved[\"api_mode\"] == \"bedrock_converse\"\n     assert resolved.get(\"bedrock_anthropic\") is not True\n+\n+\n+def test_auto_provider_with_local_base_url_bypasses_anthropic_key(monkeypatch):\n+    \"\"\"provider:auto + base_url:localhost should NOT route to Anthropic even if\n+    ANTHROPIC_API_KEY is set in the environment. Regression test for #3846.\n+\n+    Users running Ollama locally had requests silently sent to Anthropic's API\n+    because resolve_provider(\"auto\") picked up ANTHROPIC_API_KEY before the\n+    config.yaml base_url was checked.\n+    \"\"\"\n+    # ANTHROPIC_API_KEY is present in environment — should be ignored\n+    monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-fake-key\")\n+    monkeypatch.delenv(\"OPENAI_BASE_URL\", raising=False)\n+    monkeypatch.delenv(\"OPENROUTER_BASE_URL\", raising=False)\n+    monkeypatch.delenv(\"OPENAI_API_KEY\", raising=False)\n+    monkeypatch.delenv(\"OPENROUTER_API_KEY\", raising=False)\n+\n+    monkeypatch.setattr(\n+        rp,\n+        \"_get_model_config\",\n+        lambda: {\n+            \"default\": \"ollama/minimax-m2.7:cloud\",\n+            \"provider\": \"auto\",\n+            \"base_url\": \"http://localhost:11434\",\n+        },\n+    )\n+\n+    resolved = rp.resolve_runtime_provider()\n+\n+    # Must NOT go to Anthropic's API\n+    assert \"anthropic.com\" not in resolved.get(\"base_url\", \"\"), (\n+        f\"Expected custom endpoint, got Anthropic: {resolved}\"\n+    )\n+    assert resolved[\"base_url\"] == \"http://localhost:11434\"\n+    # provider should be custom/openrouter (OpenAI-compat), not anthropic\n+    assert resolved[\"provider\"] != \"anthropic\", (\n+        f\"Should have routed to custom endpoint, not anthropic: {resolved}\"\n+    )\n+\n+\n+def test_auto_provider_with_known_cloud_base_url_still_uses_anthropic(monkeypatch):\n+    \"\"\"provider:auto + base_url pointing to Anthropic should still use Anthropic.\n+\n+    The local-endpoint bypass only applies to non-cloud endpoints; when the\n+    configured base_url IS a cloud API root, resolve_provider() must run\n+    normally and pick up ANTHROPIC_API_KEY.\n+    \"\"\"\n+    monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-fake-key\")\n+    monkeypatch.delenv(\"OPENAI_BASE_URL\", raising=False)\n+    monkeypatch.delenv(\"OPENROUTER_BASE_URL\", raising=False)\n+    monkeypatch.delenv(\"OPENAI_API_KEY\", raising=False)\n+    monkeypatch.delenv(\"OPENROUTER_API_KEY\", raising=False)\n+\n+    monkeypatch.setattr(\n+        rp,\n+        \"_get_model_config\",\n+        lambda: {\n+            \"provider\": \"auto\",\n+            \"base_url\": \"https://api.anthropic.com\",\n+        },\n+    )\n+\n+    resolved = rp.resolve_runtime_provider()\n+\n+    # Cloud base_url → bypass does NOT fire → resolve_provider picks anthropic.\n+    assert resolved[\"provider\"] == \"anthropic\", (\n+        f\"Cloud base_url must not be diverted to the custom resolver: {resolved}\"\n+    )\n+\n+\n+def test_auto_provider_lookalike_cloud_host_does_not_bypass_to_cloud(monkeypatch):\n+    \"\"\"A look-alike host (api.anthropic.com.attacker.test) must be treated as a\n+    custom endpoint, NOT mistaken for the real Anthropic cloud.\n+\n+    Guards the host-match (vs naive substring) check in the local-endpoint\n+    bypass: substring matching on \"api.anthropic.com\" would wrongly classify\n+    this attacker-controlled host as cloud and hand it the ANTHROPIC_API_KEY.\n+    \"\"\"\n+    monkeypatch.setenv(\"ANTHROPIC_API_KEY\", \"sk-ant-fake-key\")\n+    monkeypatch.delenv(\"OPENAI_BASE_URL\", raising=False)\n+    monkeypatch.delenv(\"OPENROUTER_BASE_URL\", raising=False)\n+    monkeypatch.delenv(\"OPENAI_API_KEY\", raising=False)\n+    monkeypatch.delenv(\"OPENROUTER_API_KEY\", raising=False)\n+\n+    lookalike = \"http://api.anthropic.com.attacker.test/v1\"\n+    monkeypatch.setattr(\n+        rp,\n+        \"_get_model_config\",\n+        lambda: {\"provider\": \"auto\", \"base_url\": lookalike},\n+    )\n+\n+    resolved = rp.resolve_runtime_provider()\n+\n+    # Host doesn't actually match anthropic.com → bypass fires → custom route,\n+    # and the real Anthropic credential is NOT sent there.\n+    assert resolved[\"provider\"] != \"anthropic\", (\n+        f\"Look-alike host must not be classified as Anthropic cloud: {resolved}\"\n+    )\n+    assert resolved[\"base_url\"] == lookalike\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}