NousResearch/hermes-agent

Doctor suppressing stale direct-key issues with OAuth

Cluster 26693. Data as of

Nothing open on GitHub. Fix merged in #26853 on 16 May 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #26853. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #26853. That is separate from the fix event.
Still open
Nothing open on GitHub.

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.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #26853 fix(doctor): suppress stale direct-key issues when oauth is healthy (salvage of #26704)

    merged to main

    files: hermes_cli/doctor.py, scripts/release.py, tests/hermes_cli/test_doctor.py

    Show the diff
    • hermes_cli/doctor.py
      @@ -152,6 +152,30 @@ def _apply_doctor_tool_availability_overrides(available: list[str], unavailable:
           return updated_available, updated_unavailable
       
       
      +def _has_healthy_oauth_fallback_for_apikey_provider(provider_label: str) -> bool:
      +    """Return True when a direct API-key probe failure is non-blocking.
      +
      +    Some provider families support both a direct API-key path and a separate
      +    OAuth runtime path. When the OAuth path is already healthy, doctor should
      +    still show a failed API-key connectivity row, but it should not promote
      +    that direct-key problem into the final blocking summary.
      +    """
      +    try:
      +        from hermes_cli.auth import (
      +            get_gemini_oauth_auth_status,
      +            get_minimax_oauth_auth_status,
      +        )
      +    except Exception:
      +        return False
      +
      +    normalized = (provider_label or "").strip().lower()
      +    if normalized in {"google / gemini", "gemini"}:
      +        return bool((get_gemini_oauth_auth_status() or {}).get("logged_in"))
      +    if normalized == "minimax":
      +        return bool((get_minimax_oauth_auth_status() or {}).get("logged_in"))
      +    return False
      +
      +
       def check_ok(text: str, detail: str = ""):
           print(f"  {color('✓', Colors.GREEN)} {text}" + (f" {color(detail, Colors.DIM)}" if detail else ""))
       
      @@ -1594,7 +1618,10 @@ def _probe_bedrock() -> _ConnectivityResult:
                       print(f"  {_glyph} {_label} {_detail}")
                   else:
                       print(f"  {_glyph} {_label}")
      -        for _issue in _r.issues:
      +        _issues_to_add = list(_r.issues)
      +        if _issues_to_add and _has_healthy_oauth_fallback_for_apikey_provider(_r.label):
      +            _issues_to_add = []
      +        for _issue in _issues_to_add:
                   issues.append(_issue)
       
           # =========================================================================
    • scripts/release.py
      @@ -92,6 +92,7 @@
           "[email protected]": "1000Delta",
           "[redacted:email]": "szymonclawd",
           "[email protected]": "szymonclawd",
      +    "[email protected]": "worlldz",
           "[redacted:email]": "luoyuctl",
           "[email protected]": "luoyuctl",
           "[email protected]": "teknium1",
    • tests/hermes_cli/test_doctor.py
      @@ -839,3 +839,108 @@ def mock_run(cmd, **kwargs):
       
               assert "gh auth" in str(call_log) or any(c[0] == "gh" for c in call_log), f"gh not called: {call_log}"
               assert "GitHub authenticated via gh CLI" in out or "token configured" in out
      +
      +
      +def _run_doctor_with_healthy_oauth_fallback(
      +    monkeypatch,
      +    tmp_path,
      +    *,
      +    env_key: str,
      +    bad_key: str,
      +    failing_host: str,
      +    gemini_oauth_status: dict,
      +    minimax_oauth_status: dict,
      +) -> str:
      +    home = tmp_path / ".hermes"
      +    home.mkdir(parents=True, exist_ok=True)
      +    (home / "config.yaml").write_text(
      +        "model:\n"
      +        "  provider: nous\n"
      +        "  default: moonshotai/kimi-k2.6\n",
      +        encoding="utf-8",
      +    )
      +    project = tmp_path / "project"
      +    project.mkdir(exist_ok=True)
      +
      +    monkeypatch.setattr(doctor_mod, "HERMES_HOME", home)
      +    monkeypatch.setattr(doctor_mod, "PROJECT_ROOT", project)
      +    monkeypatch.setattr(doctor_mod, "_DHH", str(home))
      +    monkeypatch.setenv(env_key, bad_key)
      +    monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
      +    monkeypatch.delenv("OPENAI_API_KEY", raising=False)
      +    monkeypatch.delenv("GEMINI_API_KEY", raising=False)
      +    monkeypatch.delenv("GOOGLE_API_KEY", raising=False)
      +    monkeypatch.delenv("MINIMAX_API_KEY", raising=False)
      +    monkeypatch.delenv("MINIMAX_CN_API_KEY", raising=False)
      +    monkeypatch.setenv(env_key, bad_key)
      +
      +    fake_model_tools = types.SimpleNamespace(
      +        check_tool_availability=lambda *a, **kw: ([], []),
      +        TOOLSET_REQUIREMENTS={},
      +    )
      +    monkeypatch.setitem(sys.modules, "model_tools", fake_model_tools)
      +
      +    from hermes_cli import auth as _auth_mod
      +
      +    monkeypatch.setattr(_auth_mod, "get_nous_auth_status", lambda: {"logged_in": True})
      +    monkeypatch.setattr(_auth_mod, "get_codex_auth_status", lambda: {})
      +    monkeypatch.setattr(_auth_mod, "get_gemini_oauth_auth_status", lambda: gemini_oauth_status)
      +    monkeypatch.setattr(_auth_mod, "get_minimax_oauth_auth_status", lambda: minimax_oauth_status)
      +
      +    def fake_get(url, headers=None, timeout=None):
      +        status = 401 if failing_host in url else 200
      +        return types.SimpleNamespace(status_code=status)
      +
      +    import httpx
      +
      +    monkeypatch.setattr(httpx, "get", fake_get)
      +
      +    buf = io.StringIO()
      +    with contextlib.redirect_stdout(buf):
      +        doctor_mod.run_doctor(Namespace(fix=False))
      +    return buf.getvalue()
      +
      +
      [redacted:email](
      +    ("env_key", "bad_key", "failing_host", "gemini_oauth_status", "minimax_oauth_status", "unexpected_issue"),
      +    [
      +        (
      +            "GOOGLE_API_KEY",
      +            "bad-gemini-key",
      +            "googleapis.com",
      +            {"logged_in": True, "email": "[redacted:email]"},
      +            {},
      +            "Check GOOGLE_API_KEY in .env",
      +        ),
      +        (
      +            "MINIMAX_API_KEY",
      +            "bad-minimax-key",
      +            "minimax.io",
      +            {},
      +            {"logged_in": True, "region": "global"},
      +            "Check MINIMAX_API_KEY in .env",
      +        ),
      +    ],
      +)
      +def test_run_doctor_ignores_invalid_direct_keys_when_oauth_fallback_is_healthy(
      +    monkeypatch,
      +    tmp_path,
      +    env_key,
      +    bad_key,
      +    failing_host,
      +    gemini_oauth_status,
      +    minimax_oauth_status,
      +    unexpected_issue,
      +):
      +    out = _run_doctor_with_healthy_oauth_fallback(
      +        monkeypatch,
      +        tmp_path,
      +        env_key=env_key,
      +        bad_key=bad_key,
      +        failing_host=failing_host,
      +        gemini_oauth_status=gemini_oauth_status,
      +        minimax_oauth_status=minimax_oauth_status,
      +    )
      +
      +    assert "invalid API key" in out
      +    assert unexpected_issue not in out

1 PR closed without merging

From the close comments: done elsewhere 1

  • #26704 fix(doctor): suppress stale direct-key issues when oauth is healthy · done elsewhere → d0a183c, c4bda3f, #26853
Closed issues · 1
  • Closed#26693[Bug]: hermes doctor promotes invalid direct API keys into the final summary even when OAuth fallback is already healthy
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Merged PRs
#26853
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
16 May 2026

For your agent

The same facts, typed.