{"repo":"NousResearch/hermes-agent","public_id":"26693","display_label":"Doctor suppressing stale direct-key issues with OAuth","summary":"These members address a bug where the doctor command surfaces invalid direct API key warnings even when OAuth is functioning correctly. The fix involves suppressing those stale direct-key issues when OAuth is healthy.","subsystem":null,"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":26853,"merged_at":"2026-05-16T09:21:18+00:00"},"resolution":{"state":"resolved","by":"pr-26853"},"freshness":{"state":"verified","last_verified_at":"2026-05-16T09:21:18+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":26693,"pr_number":26853,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-05-16T09:21:18+00:00"}],"members":[{"kind":"issue","number":26693,"title":"[Bug]: hermes doctor promotes invalid direct API keys into the final summary even when OAuth fallback is already healthy","url":null,"state":"closed"},{"kind":"pr","number":26704,"title":"fix(doctor): suppress stale direct-key issues when oauth is healthy","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"d0a183cad","number":0},{"kind":"commit","value":"c4bda3f27","number":0},{"kind":"pr","value":"#26853","number":26853}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":26853,"title":"fix(doctor): suppress stale direct-key issues when oauth is healthy (salvage of #26704)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/doctor.py","scripts/release.py","tests/hermes_cli/test_doctor.py"],"fix_hunks":[{"file":"hermes_cli/doctor.py","hunks":["@@ -152,6 +152,30 @@ def _apply_doctor_tool_availability_overrides(available: list[str], unavailable:\n     return updated_available, updated_unavailable\n \n \n+def _has_healthy_oauth_fallback_for_apikey_provider(provider_label: str) -> bool:\n+    \"\"\"Return True when a direct API-key probe failure is non-blocking.\n+\n+    Some provider families support both a direct API-key path and a separate\n+    OAuth runtime path. When the OAuth path is already healthy, doctor should\n+    still show a failed API-key connectivity row, but it should not promote\n+    that direct-key problem into the final blocking summary.\n+    \"\"\"\n+    try:\n+        from hermes_cli.auth import (\n+            get_gemini_oauth_auth_status,\n+            get_minimax_oauth_auth_status,\n+        )\n+    except Exception:\n+        return False\n+\n+    normalized = (provider_label or \"\").strip().lower()\n+    if normalized in {\"google / gemini\", \"gemini\"}:\n+        return bool((get_gemini_oauth_auth_status() or {}).get(\"logged_in\"))\n+    if normalized == \"minimax\":\n+        return bool((get_minimax_oauth_auth_status() or {}).get(\"logged_in\"))\n+    return False\n+\n+\n def check_ok(text: str, detail: str = \"\"):\n     print(f\"  {color('✓', Colors.GREEN)} {text}\" + (f\" {color(detail, Colors.DIM)}\" if detail else \"\"))\n ","@@ -1594,7 +1618,10 @@ def _probe_bedrock() -> _ConnectivityResult:\n                 print(f\"  {_glyph} {_label} {_detail}\")\n             else:\n                 print(f\"  {_glyph} {_label}\")\n-        for _issue in _r.issues:\n+        _issues_to_add = list(_r.issues)\n+        if _issues_to_add and _has_healthy_oauth_fallback_for_apikey_provider(_r.label):\n+            _issues_to_add = []\n+        for _issue in _issues_to_add:\n             issues.append(_issue)\n \n     # ========================================================================="]},{"file":"scripts/release.py","hunks":["@@ -92,6 +92,7 @@\n     \"30397170+1000Delta@users.noreply.github.com\": \"1000Delta\",\n     \"[redacted:email]\": \"szymonclawd\",\n     \"257759490+szymonclawd@users.noreply.github.com\": \"szymonclawd\",\n+    \"101180447+worlldz@users.noreply.github.com\": \"worlldz\",\n     \"[redacted:email]\": \"luoyuctl\",\n     \"51604064+luoyuctl@users.noreply.github.com\": \"luoyuctl\",\n     \"127238744+teknium1@users.noreply.github.com\": \"teknium1\","]},{"file":"tests/hermes_cli/test_doctor.py","hunks":["@@ -839,3 +839,108 @@ def mock_run(cmd, **kwargs):\n \n         assert \"gh auth\" in str(call_log) or any(c[0] == \"gh\" for c in call_log), f\"gh not called: {call_log}\"\n         assert \"GitHub authenticated via gh CLI\" in out or \"token configured\" in out\n+\n+\n+def _run_doctor_with_healthy_oauth_fallback(\n+    monkeypatch,\n+    tmp_path,\n+    *,\n+    env_key: str,\n+    bad_key: str,\n+    failing_host: str,\n+    gemini_oauth_status: dict,\n+    minimax_oauth_status: dict,\n+) -> str:\n+    home = tmp_path / \".hermes\"\n+    home.mkdir(parents=True, exist_ok=True)\n+    (home / \"config.yaml\").write_text(\n+        \"model:\\n\"\n+        \"  provider: nous\\n\"\n+        \"  default: moonshotai/kimi-k2.6\\n\",\n+        encoding=\"utf-8\",\n+    )\n+    project = tmp_path / \"project\"\n+    project.mkdir(exist_ok=True)\n+\n+    monkeypatch.setattr(doctor_mod, \"HERMES_HOME\", home)\n+    monkeypatch.setattr(doctor_mod, \"PROJECT_ROOT\", project)\n+    monkeypatch.setattr(doctor_mod, \"_DHH\", str(home))\n+    monkeypatch.setenv(env_key, bad_key)\n+    monkeypatch.delenv(\"OPENROUTER_API_KEY\", raising=False)\n+    monkeypatch.delenv(\"OPENAI_API_KEY\", raising=False)\n+    monkeypatch.delenv(\"GEMINI_API_KEY\", raising=False)\n+    monkeypatch.delenv(\"GOOGLE_API_KEY\", raising=False)\n+    monkeypatch.delenv(\"MINIMAX_API_KEY\", raising=False)\n+    monkeypatch.delenv(\"MINIMAX_CN_API_KEY\", raising=False)\n+    monkeypatch.setenv(env_key, bad_key)\n+\n+    fake_model_tools = types.SimpleNamespace(\n+        check_tool_availability=lambda *a, **kw: ([], []),\n+        TOOLSET_REQUIREMENTS={},\n+    )\n+    monkeypatch.setitem(sys.modules, \"model_tools\", fake_model_tools)\n+\n+    from hermes_cli import auth as _auth_mod\n+\n+    monkeypatch.setattr(_auth_mod, \"get_nous_auth_status\", lambda: {\"logged_in\": True})\n+    monkeypatch.setattr(_auth_mod, \"get_codex_auth_status\", lambda: {})\n+    monkeypatch.setattr(_auth_mod, \"get_gemini_oauth_auth_status\", lambda: gemini_oauth_status)\n+    monkeypatch.setattr(_auth_mod, \"get_minimax_oauth_auth_status\", lambda: minimax_oauth_status)\n+\n+    def fake_get(url, headers=None, timeout=None):\n+        status = 401 if failing_host in url else 200\n+        return types.SimpleNamespace(status_code=status)\n+\n+    import httpx\n+\n+    monkeypatch.setattr(httpx, \"get\", fake_get)\n+\n+    buf = io.StringIO()\n+    with contextlib.redirect_stdout(buf):\n+        doctor_mod.run_doctor(Namespace(fix=False))\n+    return buf.getvalue()\n+\n+\n[redacted:email](\n+    (\"env_key\", \"bad_key\", \"failing_host\", \"gemini_oauth_status\", \"minimax_oauth_status\", \"unexpected_issue\"),\n+    [\n+        (\n+            \"GOOGLE_API_KEY\",\n+            \"bad-gemini-key\",\n+            \"googleapis.com\",\n+            {\"logged_in\": True, \"email\": \"[redacted:email]\"},\n+            {},\n+            \"Check GOOGLE_API_KEY in .env\",\n+        ),\n+        (\n+            \"MINIMAX_API_KEY\",\n+            \"bad-minimax-key\",\n+            \"minimax.io\",\n+            {},\n+            {\"logged_in\": True, \"region\": \"global\"},\n+            \"Check MINIMAX_API_KEY in .env\",\n+        ),\n+    ],\n+)\n+def test_run_doctor_ignores_invalid_direct_keys_when_oauth_fallback_is_healthy(\n+    monkeypatch,\n+    tmp_path,\n+    env_key,\n+    bad_key,\n+    failing_host,\n+    gemini_oauth_status,\n+    minimax_oauth_status,\n+    unexpected_issue,\n+):\n+    out = _run_doctor_with_healthy_oauth_fallback(\n+        monkeypatch,\n+        tmp_path,\n+        env_key=env_key,\n+        bad_key=bad_key,\n+        failing_host=failing_host,\n+        gemini_oauth_status=gemini_oauth_status,\n+        minimax_oauth_status=minimax_oauth_status,\n+    )\n+\n+    assert \"invalid API key\" in out\n+    assert unexpected_issue not in out\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}