{"repo":"NousResearch/hermes-agent","public_id":"2384","display_label":"Anthropic fallback incorrectly inheriting non-Anthropic base_url","summary":"These issues and fixes address a bug where the Anthropic fallback provider could inherit a base_url from another model (e.g., Codex), causing Claude requests to be sent to the wrong endpoint. A secondary fix for resetting on stash conflict is also bundled in one PR.","subsystem":"agent","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":2388,"merged_at":"2026-03-21T23:20:08+00:00"},"resolution":{"state":"resolved","by":"pr-2388"},"freshness":{"state":"stale","last_verified_at":"2026-03-21T23:20:08+00:00","is_stale":true},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":2374,"pr_number":2388,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-03-21T23:20:08+00:00"},{"issue_number":2384,"pr_number":2388,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-03-21T23:20:08+00:00"}],"members":[{"kind":"issue","number":2384,"title":"Anthropic fallback can inherit Codex model.base_url and send Claude requests to chatgpt.com/backend-api/codex","url":null,"state":"closed"},{"kind":"pr","number":2385,"title":"fix: Anthropic fallback can inherit Codex model.base_url and send Clau","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#2388","number":2388}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":2388,"title":"fix(provider): prevent Anthropic fallback from inheriting non-Anthropic base_url + fix(update): reset on stash conflict","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/auxiliary_client.py","hermes_cli/main.py","hermes_cli/runtime_provider.py","tests/hermes_cli/test_update_autostash.py"],"fix_hunks":[{"file":"agent/auxiliary_client.py","hunks":["@@ -654,16 +654,20 @@ def _try_anthropic() -> Tuple[Optional[Any], Optional[str]]:\n     if not token:\n         return None, None\n \n-    # Allow base URL override from config.yaml model.base_url\n+    # Allow base URL override from config.yaml model.base_url, but only\n+    # when the configured provider is anthropic — otherwise a non-Anthropic\n+    # base_url (e.g. Codex endpoint) would leak into Anthropic requests.\n     base_url = _ANTHROPIC_DEFAULT_BASE_URL\n     try:\n         from hermes_cli.config import load_config\n         cfg = load_config()\n         model_cfg = cfg.get(\"model\")\n         if isinstance(model_cfg, dict):\n-            cfg_base_url = (model_cfg.get(\"base_url\") or \"\").strip().rstrip(\"/\")\n-            if cfg_base_url:\n-                base_url = cfg_base_url\n+            cfg_provider = str(model_cfg.get(\"provider\") or \"\").strip().lower()\n+            if cfg_provider == \"anthropic\":\n+                cfg_base_url = (model_cfg.get(\"base_url\") or \"\").strip().rstrip(\"/\")\n+                if cfg_base_url:\n+                    base_url = cfg_base_url\n     except Exception:\n         pass\n "]},{"file":"hermes_cli/main.py","hunks":["@@ -2559,12 +2559,29 @@ def _restore_stashed_changes(\n         capture_output=True,\n         text=True,\n     )\n-    if restore.returncode != 0:\n+\n+    # Check for unmerged (conflicted) files — can happen even when returncode is 0\n+    unmerged = subprocess.run(\n+        git_cmd + [\"diff\", \"--name-only\", \"--diff-filter=U\"],\n+        cwd=cwd,\n+        capture_output=True,\n+        text=True,\n+    )\n+    has_conflicts = bool(unmerged.stdout.strip())\n+\n+    if restore.returncode != 0 or has_conflicts:\n+        # Reset the working tree so Hermes is runnable with the updated code\n+        subprocess.run(\n+            git_cmd + [\"reset\", \"--hard\", \"HEAD\"],\n+            cwd=cwd,\n+            capture_output=True,\n+        )\n         print(\"✗ Update pulled new code, but restoring local changes failed.\")\n         if restore.stdout.strip():\n             print(restore.stdout.strip())\n         if restore.stderr.strip():\n             print(restore.stderr.strip())\n+        print(\"The working tree has been reset to a clean state.\")\n         print(\"Your changes are still preserved in git stash.\")\n         print(f\"Resolve manually with: git stash apply {stash_ref}\")\n         sys.exit(1)"]},{"file":"hermes_cli/runtime_provider.py","hunks":["@@ -359,9 +359,14 @@ def resolve_runtime_provider(\n                 \"No Anthropic credentials found. Set ANTHROPIC_TOKEN or ANTHROPIC_API_KEY, \"\n                 \"run 'claude setup-token', or authenticate with 'claude /login'.\"\n             )\n-        # Allow base URL override from config.yaml model.base_url\n+        # Allow base URL override from config.yaml model.base_url, but only\n+        # when the configured provider is anthropic — otherwise a non-Anthropic\n+        # base_url (e.g. Codex endpoint) would leak into Anthropic requests.\n         model_cfg = _get_model_config()\n-        cfg_base_url = (model_cfg.get(\"base_url\") or \"\").strip().rstrip(\"/\")\n+        cfg_provider = str(model_cfg.get(\"provider\") or \"\").strip().lower()\n+        cfg_base_url = \"\"\n+        if cfg_provider == \"anthropic\":\n+            cfg_base_url = (model_cfg.get(\"base_url\") or \"\").strip().rstrip(\"/\")\n         base_url = cfg_base_url or \"https://api.anthropic.com\"\n         return {\n             \"provider\": \"anthropic\","]},{"file":"tests/hermes_cli/test_update_autostash.py","hunks":["@@ -68,6 +68,8 @@ def fake_run(cmd, **kwargs):\n         calls.append((cmd, kwargs))\n         if cmd[1:3] == [\"stash\", \"apply\"]:\n             return SimpleNamespace(stdout=\"applied\\n\", stderr=\"\", returncode=0)\n+        if cmd[1:3] == [\"diff\", \"--name-only\"]:\n+            return SimpleNamespace(stdout=\"\", stderr=\"\", returncode=0)\n         if cmd[1:3] == [\"stash\", \"list\"]:\n             return SimpleNamespace(stdout=\"stash@{1} abc123\\n\", stderr=\"\", returncode=0)\n         if cmd[1:3] == [\"stash\", \"drop\"]:","@@ -81,8 +83,9 @@ def fake_run(cmd, **kwargs):\n \n     assert restored is True\n     assert calls[0][0] == [\"git\", \"stash\", \"apply\", \"abc123\"]\n-    assert calls[1][0] == [\"git\", \"stash\", \"list\", \"--format=%gd %H\"]\n-    assert calls[2][0] == [\"git\", \"stash\", \"drop\", \"stash@{1}\"]\n+    assert calls[1][0] == [\"git\", \"diff\", \"--name-only\", \"--diff-filter=U\"]\n+    assert calls[2][0] == [\"git\", \"stash\", \"list\", \"--format=%gd %H\"]\n+    assert calls[3][0] == [\"git\", \"stash\", \"drop\", \"stash@{1}\"]\n     out = capsys.readouterr().out\n     assert \"Restore local changes now? [Y/n]\" in out\n     assert \"restored on top of the updated codebase\" in out","@@ -117,6 +120,8 @@ def fake_run(cmd, **kwargs):\n         calls.append((cmd, kwargs))\n         if cmd[1:3] == [\"stash\", \"apply\"]:\n             return SimpleNamespace(stdout=\"applied\\n\", stderr=\"\", returncode=0)\n+        if cmd[1:3] == [\"diff\", \"--name-only\"]:\n+            return SimpleNamespace(stdout=\"\", stderr=\"\", returncode=0)\n         if cmd[1:3] == [\"stash\", \"list\"]:\n             return SimpleNamespace(stdout=\"stash@{0} abc123\\n\", stderr=\"\", returncode=0)\n         if cmd[1:3] == [\"stash\", \"drop\"]:","@@ -129,8 +134,9 @@ def fake_run(cmd, **kwargs):\n \n     assert restored is True\n     assert calls[0][0] == [\"git\", \"stash\", \"apply\", \"abc123\"]\n-    assert calls[1][0] == [\"git\", \"stash\", \"list\", \"--format=%gd %H\"]\n-    assert calls[2][0] == [\"git\", \"stash\", \"drop\", \"stash@{0}\"]\n+    assert calls[1][0] == [\"git\", \"diff\", \"--name-only\", \"--diff-filter=U\"]\n+    assert calls[2][0] == [\"git\", \"stash\", \"list\", \"--format=%gd %H\"]\n+    assert calls[3][0] == [\"git\", \"stash\", \"drop\", \"stash@{0}\"]\n     assert \"Restore local changes now?\" not in capsys.readouterr().out\n \n ","@@ -152,6 +158,8 @@ def fake_run(cmd, **kwargs):\n         calls.append((cmd, kwargs))\n         if cmd[1:3] == [\"stash\", \"apply\"]:\n             return SimpleNamespace(stdout=\"applied\\n\", stderr=\"\", returncode=0)\n+        if cmd[1:3] == [\"diff\", \"--name-only\"]:\n+            return SimpleNamespace(stdout=\"\", stderr=\"\", returncode=0)\n         if cmd[1:3] == [\"stash\", \"list\"]:\n             return SimpleNamespace(stdout=\"stash@{0} def456\\n\", stderr=\"\", returncode=0)\n         raise AssertionError(f\"unexpected command: {cmd}\")","@@ -161,10 +169,9 @@ def fake_run(cmd, **kwargs):\n     restored = hermes_main._restore_stashed_changes([\"git\"], tmp_path, \"abc123\", prompt_user=False)\n \n     assert restored is True\n-    assert calls == [\n-        ([\"git\", \"stash\", \"apply\", \"abc123\"], {\"cwd\": tmp_path, \"capture_output\": True, \"text\": True}),\n-        ([\"git\", \"stash\", \"list\", \"--format=%gd %H\"], {\"cwd\": tmp_path, \"capture_output\": True, \"text\": True, \"check\": True}),\n-    ]\n+    assert calls[0] == ([\"git\", \"stash\", \"apply\", \"abc123\"], {\"cwd\": tmp_path, \"capture_output\": True, \"text\": True})\n+    assert calls[1] == ([\"git\", \"diff\", \"--name-only\", \"--diff-filter=U\"], {\"cwd\": tmp_path, \"capture_output\": True, \"text\": True})\n+    assert calls[2] == ([\"git\", \"stash\", \"list\", \"--format=%gd %H\"], {\"cwd\": tmp_path, \"capture_output\": True, \"text\": True, \"check\": True})\n     out = capsys.readouterr().out\n     assert \"couldn't find the stash entry to drop\" in out\n     assert \"stash was left in place\" in out","@@ -181,6 +188,8 @@ def fake_run(cmd, **kwargs):\n         calls.append((cmd, kwargs))\n         if cmd[1:3] == [\"stash\", \"apply\"]:\n             return SimpleNamespace(stdout=\"applied\\n\", stderr=\"\", returncode=0)\n+        if cmd[1:3] == [\"diff\", \"--name-only\"]:\n+            return SimpleNamespace(stdout=\"\", stderr=\"\", returncode=0)\n         if cmd[1:3] == [\"stash\", \"list\"]:\n             return SimpleNamespace(stdout=\"stash@{0} abc123\\n\", stderr=\"\", returncode=0)\n         if cmd[1:3] == [\"stash\", \"drop\"]:","@@ -192,7 +201,7 @@ def fake_run(cmd, **kwargs):\n     restored = hermes_main._restore_stashed_changes([\"git\"], tmp_path, \"abc123\", prompt_user=False)\n \n     assert restored is True\n-    assert calls[2][0] == [\"git\", \"stash\", \"drop\", \"stash@{0}\"]\n+    assert calls[3][0] == [\"git\", \"stash\", \"drop\", \"stash@{0}\"]\n     out = capsys.readouterr().out\n     assert \"couldn't drop the saved stash entry\" in out\n     assert \"drop failed\" in out","@@ -208,6 +217,10 @@ def fake_run(cmd, **kwargs):\n         calls.append((cmd, kwargs))\n         if cmd[1:3] == [\"stash\", \"apply\"]:\n             return SimpleNamespace(stdout=\"conflict output\\n\", stderr=\"conflict stderr\\n\", returncode=1)\n+        if cmd[1:3] == [\"diff\", \"--name-only\"]:\n+            return SimpleNamespace(stdout=\"hermes_cli/main.py\\n\", stderr=\"\", returncode=0)\n+        if cmd[1:3] == [\"reset\", \"--hard\"]:\n+            return SimpleNamespace(stdout=\"\", stderr=\"\", returncode=0)\n         raise AssertionError(f\"unexpected command: {cmd}\")\n \n     monkeypatch.setattr(hermes_main.subprocess, \"run\", fake_run)","@@ -219,7 +232,34 @@ def fake_run(cmd, **kwargs):\n     out = capsys.readouterr().out\n     assert \"Your changes are still preserved in git stash.\" in out\n     assert \"git stash apply abc123\" in out\n-    assert calls == [([\"git\", \"stash\", \"apply\", \"abc123\"], {\"cwd\": tmp_path, \"capture_output\": True, \"text\": True})]\n+    assert \"working tree has been reset to a clean state\" in out\n+    # Verify reset --hard was called to clean up conflict markers\n+    reset_calls = [c for c, _ in calls if c[1:3] == [\"reset\", \"--hard\"]]\n+    assert len(reset_calls) == 1\n+\n+\n+def test_restore_stashed_changes_resets_when_unmerged_files_detected(monkeypatch, tmp_path, capsys):\n+    \"\"\"Even if stash apply returns 0, conflict markers must be cleaned up.\"\"\"\n+    calls = []\n+\n+    def fake_run(cmd, **kwargs):\n+        calls.append((cmd, kwargs))\n+        if cmd[1:3] == [\"stash\", \"apply\"]:\n+            return SimpleNamespace(stdout=\"applied\\n\", stderr=\"\", returncode=0)\n+        if cmd[1:3] == [\"diff\", \"--name-only\"]:\n+            return SimpleNamespace(stdout=\"cli.py\\n\", stderr=\"\", returncode=0)\n+        if cmd[1:3] == [\"reset\", \"--hard\"]:\n+            return SimpleNamespace(stdout=\"\", stderr=\"\", returncode=0)\n+        raise AssertionError(f\"unexpected command: {cmd}\")\n+\n+    monkeypatch.setattr(hermes_main.subprocess, \"run\", fake_run)\n+\n+    with pytest.raises(SystemExit, match=\"1\"):\n+        hermes_main._restore_stashed_changes([\"git\"], tmp_path, \"abc123\", prompt_user=False)\n+\n+    out = capsys.readouterr().out\n+    assert \"working tree has been reset to a clean state\" in out\n+    assert \"git stash apply abc123\" in out\n \n \n def test_stash_local_changes_if_needed_raises_when_stash_ref_missing(monkeypatch, tmp_path):\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}