# NousResearch/hermes-agent · Gemini auth method fixes in doctor/discovery

## Open now

- 2 issues, 2 PRs open on GitHub.
- #48689: \`hermes doctor\` reports stale \`ui-tui\` npm vulnerability and false-positive \`gemini (invalid API key)\`
- #62259: Gemini live model discovery fails — native /v1beta endpoint rejects Bearer auth (falls back to 4 static models)
- PR #48752: fix: reduce stale doctor audit and oauth noise
- PR #116509: fix(providers): complete native Gemini ListModels discovery

These issues and PRs center on incorrect authentication formats (Bearer token vs. API key query param or x-goog-api-key header) being used when probing or discovering Gemini endpoints in the doctor command and model discovery. Multiple related fixes address false-positive invalid-key errors and routing to the correct Gemini API endpoint.

## TL;DR

- 30 member(s): 2 open issue(s), 10 closed issue(s), 2 open PR(s), 2 merged PR(s), 14 PR(s) closed without merging.
- A fix was merged to main in PR #11961. Verify it addresses your case.
- **Recommended:** read PR #11961 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-22801`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.07`: the share of member 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.

## Open related issues

- #48689: \`hermes doctor\` reports stale \`ui-tui\` npm vulnerability and false-positive \`gemini (invalid API key)\`
- #62259: Gemini live model discovery fails — native /v1beta endpoint rejects Bearer auth (falls back to 4 static models)

## Closed / duplicate issues

- #19837: \[Bug\]: cli doctor API Connectivity missing google api key/gemini api key
- #21058: hermes doctor: Gemini healthcheck fails with 401 due to Bearer-token auth incompatibility
- #21481: \[Bug\]: hermes doctor falsely reports valid Google AI Studio Gemini API key as invalid
- #22223: doctor should honor provider base URL overrides for Xiaomi health checks
- #22346: hermes doctor: duplicate \`anthropic\` check fails with HTTP 404 (wrong auth header in pluggable provider check)
- #23354: Hermes Doctor flags Google Gemini API key as invalid even when the key works
- #25108: \[Bug\]: hermes doctor validates Gemini API keys with Bearer auth instead of ?key=
- #26284: hermes doctor reports false positive "invalid API key" for Google Gemini
- #26623: \[Bug\]: bug(cli): hermes doctor falsely reports Google Gemini API key as invalid (HTTP 401 due to OpenAI auth format)
- #27310: doctor should validate Gemini API keys with Gemini native auth instead of generic Bearer probe

## Prior & competing PRs

- **#11961: fix(gemini): route Google AI Studio auth via x-goog-api-key header (#7893)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/auxiliary_client.py`, `run_agent.py`, `scripts/release.py`, `tests/hermes_cli/test_gemini_provider.py`
- **#19854: fix(doctor): report google/gemini API key status in connectivity checks**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#20204`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#20204: doctor: report Google Gemini API connectivity**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#21490: fix(doctor): validate Gemini keys with Google auth**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `364a1dd29`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#22232: fix: align doctor provider health checks**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `0c233e70f84a7598f874d6a9b31898408717eabe`, `6b21a935af24b7b3c4ee2370598471aa8978a243`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#22367: fix(doctor): skip duplicate anthropic Bearer-auth check**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#22529`, `#22346`, `#22529`, `#22529`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#22801: fix(doctor): skip pluggable provider profiles when a dedicated check exists (salvage #22529)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/doctor.py`, `tests/hermes_cli/test_doctor_dedicated_provider_skip.py`
  - fix hunks:
  - `hermes_cli/doctor.py`

    ```diff
    @@ -245,15 +245,31 @@ def _build_apikey_providers_list() -> list:
         }
         for _label, _canonical in _name_to_canonical.items():
             _known_canonical.add(_canonical)
    +    # Providers that already have a dedicated health check above the generic
    +    # API-key loop (with custom headers/auth). Skip their pluggable profiles
    +    # here so the generic Bearer-auth loop doesn't run a duplicate, broken
    +    # check (e.g. Anthropic native API requires x-api-key, not Bearer).
    +    _dedicated_canonical = {"anthropic", "openrouter", "bedrock"}
    +    _known_canonical.update(_dedicated_canonical)
         try:
             from providers import list_providers
             from providers.base import ProviderProfile as _PP
    +        try:
    +            from hermes_cli.providers import normalize_provider as _normalize_provider
    +        except Exception:  # pragma: no cover - normalization is best-effort
    +            def _normalize_provider(_name: str) -> str:
    +                return (_name or "").strip().lower()
             for _pp in list_providers():
                 if not isinstance(_pp, _PP) or _pp.auth_type != "api_key" or not _pp.env_vars:
                     continue
                 _label = _pp.display_name or _pp.name
                 if _label in _known_names or _pp.name in _known_canonical:
                     continue
    +            _candidates = {_normalize_provider(_pp.name)}
    +            for _alias in (_pp.aliases or ()):
    +                _candidates.add(_normalize_provider(_alias))
    +            if _candidates & _dedicated_canonical:
    +                continue
                 # Separate API-key vars from base-URL override vars — the health-check
                 # loop sends the first found value as Authorization: Bearer, so a URL
                 # string must never be picked.
    ```
  - `tests/hermes_cli/test_doctor_dedicated_provider_skip.py`

    ```diff
    @@ -0,0 +1,50 @@
    +"""Regression: hermes doctor must not run a generic Bearer-auth health
    +check for providers that already have a dedicated check (Anthropic,
    +OpenRouter, Bedrock).
    +
    +Anthropic's native API requires `x-api-key` + `anthropic-version` headers;
    +the generic loop sends `Authorization: Bearer ...` which Anthropic answers
    +with HTTP 404. The dedicated check at hermes_cli/doctor.py already covers
    +Anthropic with the right headers, so the pluggable profile must be
    +skipped by `_build_apikey_providers_list()`.
    +
    +See: NousResearch/hermes-agent#22346
    +"""
    +
    +from __future__ import annotations
    +
    +
    +def test_build_apikey_providers_list_skips_dedicated_check_providers():
    +    from hermes_cli import doctor
    +
    +    # Force a rebuild — the module caches the list on first call.
    +    doctor._APIKEY_PROVIDERS_CACHE = None
    +    entries = doctor._build_apikey_providers_list()
    +
    +    # Tuple shape: (display_name, env_vars, default_url, base_env, supports_health_check)
    +    names = {entry[0].lower() for entry in entries}
    +    assert not any("anthropic" in name for name in names), (
    +        f"Anthropic provider profile leaked into generic Bearer-auth health "
    +        f"check loop. Dedicated check above already covers it with "
    +        f"x-api-key headers. Got entries: {sorted(names)}"
    +    )
    +    assert not any("openrouter" in name for name in names), (
    +        f"OpenRouter has a dedicated check; generic loop must skip it. "
    +        f"Got: {sorted(names)}"
    +    )
    +    assert not any("bedrock" in name for name in names), (
    +        f"Bedrock uses AWS SDK creds, not Bearer auth; generic loop must skip. "
    +        f"Got: {sorted(names)}"
    +    )
    +
    +
    +def test_build_apikey_providers_list_includes_non_dedicated_providers():
    +    """Sanity guard: the skip-set must not strip every provider."""
    +    from hermes_cli import doctor
    +
    +    doctor._APIKEY_PROVIDERS_CACHE = None
    +    entries = doctor._build_apikey_providers_list()
    +
    +    names = {entry[0] for entry in entries}
    +    assert "DeepSeek" in names
    +    assert "Z.AI / GLM" in names
    ```
- **#20935: fix(doctor): skip /models health-check for Alibaba dedicated tiers**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#20642`, `#20642`
- **#22286: fix(doctor): probe native gemini keys correctly**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`
- **#22512: fix(cli): skip dedicated-check providers in apikey loop**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#22801`, `#22529`, `#22529`
- **#22529: fix(doctor): skip pluggable provider profiles when a dedicated check exists (#22346)**. closed without merge
  - maintainer direction: category `merged_via`; implication `superseded`; refs `#22801`
- **#23364: fix(doctor): use ?key= auth for Gemini API key validation**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#11961`, `#11961`
- **#25139: fix: use Gemini API key query auth in doctor**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`
- **#26784: fix(doctor): use query-param auth for Gemini in API-key probe**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#21058`, `#20642`, `#21490`, `#22286`, `#22468`, `#23364`, `#24033`, `#24266`, `#26067`
- **#26922: fix(doctor): use x-goog-api-key for Gemini connectivity probe (#26623)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#20642`, `#22468`, `#22489`, `#24266`, `#25883`, `#26067`
- **#48752: fix: reduce stale doctor audit and oauth noise**. open
  - maintainer direction: category `uncategorized`; implication `needs_llm`
  - files: `hermes_cli/doctor.py`, `tests/hermes_cli/test_doctor.py`
- **#62267: fix(providers): use native query-param auth for Gemini model discovery (#62259)**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#42693`, `#42693`, `#62259`
- **#116509: fix(providers): complete native Gemini ListModels discovery**. open
  - maintainer direction: category `silent_close`; implication `active`
  - files: `plugins/model-providers/gemini/__init__.py`, `tests/providers/test_fetch_models_base_url.py`

## Fix status

- Merged to main in PR #11961 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-09`.
- Subsystem: `agent`
