{"repo":"NousResearch/hermes-agent","public_id":"19837","display_label":"Gemini auth method fixes in doctor/discovery","summary":"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.","subsystem":"agent","counts":{"members":30,"open_issues":2,"open_issues_distinct":2,"open_prs":2,"closed_issues":10,"merged_prs":2,"attempt_prs":16},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":11961},"resolution":{"state":"partially_resolved","by":"pr-22801"},"freshness":{"state":"verified","last_verified_at":"2026-05-09T20:36:34+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.07181609195402296},"fix_edges":[{"issue_number":7893,"pr_number":11961,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":22346,"pr_number":22801,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true},{"issue_number":23354,"pr_number":11961,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false}],"members":[{"kind":"issue","number":19837,"title":"[Bug]: cli doctor API Connectivity missing google api key/gemini api key","url":null,"state":"closed"},{"kind":"issue","number":21058,"title":"hermes doctor: Gemini healthcheck fails with 401 due to Bearer-token auth incompatibility","url":null,"state":"closed"},{"kind":"issue","number":21481,"title":"[Bug]: hermes doctor falsely reports valid Google AI Studio Gemini API key as invalid","url":null,"state":"closed"},{"kind":"issue","number":22223,"title":"doctor should honor provider base URL overrides for Xiaomi health checks","url":null,"state":"closed"},{"kind":"issue","number":22346,"title":"hermes doctor: duplicate `anthropic` check fails with HTTP 404 (wrong auth header in pluggable provider check)","url":null,"state":"closed"},{"kind":"issue","number":23354,"title":"Hermes Doctor flags Google Gemini API key as invalid even when the key works","url":null,"state":"closed"},{"kind":"issue","number":25108,"title":"[Bug]: hermes doctor validates Gemini API keys with Bearer auth instead of ?key=","url":null,"state":"closed"},{"kind":"issue","number":26284,"title":"hermes doctor reports false positive \"invalid API key\" for Google Gemini","url":null,"state":"closed"},{"kind":"issue","number":26623,"title":"[Bug]: bug(cli): hermes doctor falsely reports Google Gemini API key as invalid (HTTP 401 due to OpenAI auth format)","url":null,"state":"closed"},{"kind":"issue","number":27310,"title":"doctor should validate Gemini API keys with Gemini native auth instead of generic Bearer probe","url":null,"state":"closed"},{"kind":"issue","number":48689,"title":"`hermes doctor` reports stale `ui-tui` npm vulnerability and false-positive `gemini (invalid API key)`","url":null,"state":"open"},{"kind":"issue","number":62259,"title":"Gemini live model discovery fails — native /v1beta endpoint rejects Bearer auth (falls back to 4 static models)","url":null,"state":"open"},{"kind":"pr","number":11961,"title":"fix(gemini): route Google AI Studio auth via x-goog-api-key header (#7893)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/auxiliary_client.py","run_agent.py","scripts/release.py","tests/hermes_cli/test_gemini_provider.py"]},{"kind":"pr","number":19854,"title":"fix(doctor): report google/gemini API key status in connectivity checks","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#20204","number":20204}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":20204,"title":"doctor: report Google Gemini API connectivity","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":20935,"title":"fix(doctor): skip /models health-check for Alibaba dedicated tiers","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#20642","number":20642},{"kind":"pr","value":"#20642","number":20642}],"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":21490,"title":"fix(doctor): validate Gemini keys with Google auth","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"364a1dd29","number":0}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":22232,"title":"fix: align doctor provider health checks","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"0c233e70f84a7598f874d6a9b31898408717eabe","number":0},{"kind":"commit","value":"6b21a935af24b7b3c4ee2370598471aa8978a243","number":0}],"category":"uncategorized","implication":"needs_llm"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":22286,"title":"fix(doctor): probe native gemini keys correctly","url":null,"state":"closed","close_reason":{"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":22367,"title":"fix(doctor): skip duplicate anthropic Bearer-auth check","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#22529","number":22529},{"kind":"pr","value":"#22346","number":22346},{"kind":"pr","value":"#22529","number":22529},{"kind":"pr","value":"#22529","number":22529}],"category":"superseded_by_pr","implication":"superseded"},"merged":false,"is_fix_pr":true,"fix_tier":"suggested","fix_detail_withheld":true},{"kind":"pr","number":22512,"title":"fix(cli): skip dedicated-check providers in apikey loop","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#22801","number":22801},{"kind":"pr","value":"#22529","number":22529},{"kind":"pr","value":"#22529","number":22529}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":22529,"title":"fix(doctor): skip pluggable provider profiles when a dedicated check exists (#22346)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#22801","number":22801}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":22801,"title":"fix(doctor): skip pluggable provider profiles when a dedicated check exists (salvage #22529)","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/doctor.py","tests/hermes_cli/test_doctor_dedicated_provider_skip.py"],"fix_hunks":[{"file":"hermes_cli/doctor.py","hunks":["@@ -245,15 +245,31 @@ def _build_apikey_providers_list() -> list:\n     }\n     for _label, _canonical in _name_to_canonical.items():\n         _known_canonical.add(_canonical)\n+    # Providers that already have a dedicated health check above the generic\n+    # API-key loop (with custom headers/auth). Skip their pluggable profiles\n+    # here so the generic Bearer-auth loop doesn't run a duplicate, broken\n+    # check (e.g. Anthropic native API requires x-api-key, not Bearer).\n+    _dedicated_canonical = {\"anthropic\", \"openrouter\", \"bedrock\"}\n+    _known_canonical.update(_dedicated_canonical)\n     try:\n         from providers import list_providers\n         from providers.base import ProviderProfile as _PP\n+        try:\n+            from hermes_cli.providers import normalize_provider as _normalize_provider\n+        except Exception:  # pragma: no cover - normalization is best-effort\n+            def _normalize_provider(_name: str) -> str:\n+                return (_name or \"\").strip().lower()\n         for _pp in list_providers():\n             if not isinstance(_pp, _PP) or _pp.auth_type != \"api_key\" or not _pp.env_vars:\n                 continue\n             _label = _pp.display_name or _pp.name\n             if _label in _known_names or _pp.name in _known_canonical:\n                 continue\n+            _candidates = {_normalize_provider(_pp.name)}\n+            for _alias in (_pp.aliases or ()):\n+                _candidates.add(_normalize_provider(_alias))\n+            if _candidates & _dedicated_canonical:\n+                continue\n             # Separate API-key vars from base-URL override vars — the health-check\n             # loop sends the first found value as Authorization: Bearer, so a URL\n             # string must never be picked."]},{"file":"tests/hermes_cli/test_doctor_dedicated_provider_skip.py","hunks":["@@ -0,0 +1,50 @@\n+\"\"\"Regression: hermes doctor must not run a generic Bearer-auth health\n+check for providers that already have a dedicated check (Anthropic,\n+OpenRouter, Bedrock).\n+\n+Anthropic's native API requires `x-api-key` + `anthropic-version` headers;\n+the generic loop sends `Authorization: Bearer ...` which Anthropic answers\n+with HTTP 404. The dedicated check at hermes_cli/doctor.py already covers\n+Anthropic with the right headers, so the pluggable profile must be\n+skipped by `_build_apikey_providers_list()`.\n+\n+See: NousResearch/hermes-agent#22346\n+\"\"\"\n+\n+from __future__ import annotations\n+\n+\n+def test_build_apikey_providers_list_skips_dedicated_check_providers():\n+    from hermes_cli import doctor\n+\n+    # Force a rebuild — the module caches the list on first call.\n+    doctor._APIKEY_PROVIDERS_CACHE = None\n+    entries = doctor._build_apikey_providers_list()\n+\n+    # Tuple shape: (display_name, env_vars, default_url, base_env, supports_health_check)\n+    names = {entry[0].lower() for entry in entries}\n+    assert not any(\"anthropic\" in name for name in names), (\n+        f\"Anthropic provider profile leaked into generic Bearer-auth health \"\n+        f\"check loop. Dedicated check above already covers it with \"\n+        f\"x-api-key headers. Got entries: {sorted(names)}\"\n+    )\n+    assert not any(\"openrouter\" in name for name in names), (\n+        f\"OpenRouter has a dedicated check; generic loop must skip it. \"\n+        f\"Got: {sorted(names)}\"\n+    )\n+    assert not any(\"bedrock\" in name for name in names), (\n+        f\"Bedrock uses AWS SDK creds, not Bearer auth; generic loop must skip. \"\n+        f\"Got: {sorted(names)}\"\n+    )\n+\n+\n+def test_build_apikey_providers_list_includes_non_dedicated_providers():\n+    \"\"\"Sanity guard: the skip-set must not strip every provider.\"\"\"\n+    from hermes_cli import doctor\n+\n+    doctor._APIKEY_PROVIDERS_CACHE = None\n+    entries = doctor._build_apikey_providers_list()\n+\n+    names = {entry[0] for entry in entries}\n+    assert \"DeepSeek\" in names\n+    assert \"Z.AI / GLM\" in names\n"]}]},{"kind":"pr","number":23364,"title":"fix(doctor): use ?key= auth for Gemini API key validation","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#11961","number":11961},{"kind":"pr","value":"#11961","number":11961}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":25139,"title":"fix: use Gemini API key query auth in doctor","url":null,"state":"closed","close_reason":{"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":26784,"title":"fix(doctor): use query-param auth for Gemini in API-key probe","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#21058","number":21058},{"kind":"pr","value":"#20642","number":20642},{"kind":"pr","value":"#21490","number":21490},{"kind":"pr","value":"#22286","number":22286},{"kind":"pr","value":"#22468","number":22468},{"kind":"pr","value":"#23364","number":23364},{"kind":"pr","value":"#24033","number":24033},{"kind":"pr","value":"#24266","number":24266},{"kind":"pr","value":"#26067","number":26067}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":26922,"title":"fix(doctor): use x-goog-api-key for Gemini connectivity probe (#26623)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#20642","number":20642},{"kind":"pr","value":"#22468","number":22468},{"kind":"pr","value":"#22489","number":22489},{"kind":"pr","value":"#24266","number":24266},{"kind":"pr","value":"#25883","number":25883},{"kind":"pr","value":"#26067","number":26067}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":48752,"title":"fix: reduce stale doctor audit and oauth noise","url":null,"state":"open","close_reason":{"category":"uncategorized","implication":"needs_llm"},"merged":false},{"kind":"pr","number":62267,"title":"fix(providers): use native query-param auth for Gemini model discovery (#62259)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#42693","number":42693},{"kind":"pr","value":"#42693","number":42693},{"kind":"pr","value":"#62259","number":62259}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":116509,"title":"fix(providers): complete native Gemini ListModels discovery","url":null,"state":"open","close_reason":{"category":"silent_close","implication":"active"},"merged":false}],"trust":{"default_tier":"verified","includes_suggested":false}}