From 1827ea3efd22a47ccea9fcc1dd532c7260928402 Mon Sep 17 00:00:00 2001 From: Michael Lam Date: Mon, 18 May 2026 19:49:54 -0700 Subject: [PATCH] fix: add Grok OAuth provider catalog support --- CHANGELOG.md | 4 ++ api/config.py | 4 ++ api/providers.py | 9 +++ tests/test_issue2545_xai_oauth_provider.py | 78 ++++++++++++++++++++++ 4 files changed, 95 insertions(+) create mode 100644 tests/test_issue2545_xai_oauth_provider.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ed15eec3..97b181c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- **PR #2568** by @Michaelyklam (closes #2545) — Add the Hermes Agent `xai-oauth` provider to the WebUI's OAuth/provider catalogs so Grok OAuth accounts authenticated through the Hermes CLI appear in Settings → Providers and the model picker. The provider is treated as CLI-managed OAuth (not WebUI API-key configurable) and uses the live Hermes CLI model catalog when available with a Grok static fallback. + ## [v0.51.91] — 2026-05-18 — Release BO (stage-384 — 5-PR full sweep batch — reasoning-replay history fix + archive-extract per-session inbox + fallback streaming warnings + sanitized custom-provider env hints + Slice 3c queue/goal adapter routing) ### Fixed diff --git a/api/config.py b/api/config.py index 846b4d33..a31dcd45 100644 --- a/api/config.py +++ b/api/config.py @@ -692,6 +692,7 @@ _PROVIDER_DISPLAY = { "anthropic": "Anthropic", "openai": "OpenAI", "openai-codex": "OpenAI Codex", + "xai-oauth": "xAI Grok OAuth", "copilot": "GitHub Copilot", "zai": "Z.AI / GLM", "kimi-coding": "Kimi / Moonshot", @@ -1209,6 +1210,9 @@ _PROVIDER_MODELS = { "x-ai": [ {"id": "grok-4.20", "label": "Grok 4.20"}, ], + "xai-oauth": [ + {"id": "grok-4.20", "label": "Grok 4.20"}, + ], } diff --git a/api/providers.py b/api/providers.py index 0b0c5c22..93233ce4 100644 --- a/api/providers.py +++ b/api/providers.py @@ -692,6 +692,7 @@ _OAUTH_PROVIDERS = frozenset({ "nous", "openai-codex", "qwen-oauth", + "xai-oauth", }) # SECTION: Helper functions @@ -1866,6 +1867,14 @@ def get_providers() -> dict[str, Any]: if live_models: models = live_models models_total = len(models) + if pid == "xai-oauth": + live_models = _models_from_live_provider_ids( + pid, + _read_live_provider_model_ids("xai-oauth"), + ) + if live_models: + models = live_models + models_total = len(models) # Nous Portal: prefer the live catalog so the providers card matches # the dropdown picker (#1538). Same fallback shape as the static-only # case below — when hermes_cli is unavailable or its lookup raises, diff --git a/tests/test_issue2545_xai_oauth_provider.py b/tests/test_issue2545_xai_oauth_provider.py new file mode 100644 index 00000000..e35e7919 --- /dev/null +++ b/tests/test_issue2545_xai_oauth_provider.py @@ -0,0 +1,78 @@ +import copy + +import api.config as config +import api.profiles as profiles + + +def _with_config(monkeypatch, tmp_path, cfg): + old_cfg = copy.deepcopy(config.cfg) + old_mtime = config._cfg_mtime + config.cfg.clear() + config.cfg.update(copy.deepcopy(cfg)) + config._cfg_mtime = 0.0 + config.invalidate_models_cache() + monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: tmp_path) + + def restore(): + config.cfg.clear() + config.cfg.update(old_cfg) + config._cfg_mtime = old_mtime + config.invalidate_models_cache() + + return restore + + +def test_xai_oauth_is_known_oauth_provider(): + from api.providers import _OAUTH_PROVIDERS + from api.config import _PROVIDER_DISPLAY + + assert "xai-oauth" in _OAUTH_PROVIDERS + assert _PROVIDER_DISPLAY["xai-oauth"] == "xAI Grok OAuth" + + +def test_xai_oauth_provider_card_uses_oauth_status_and_models(monkeypatch, tmp_path): + restore = _with_config( + monkeypatch, + tmp_path, + { + "model": {"provider": "xai-oauth", "default": "grok-4.20"}, + "providers": {}, + }, + ) + monkeypatch.setattr(config, "_read_live_provider_model_ids", lambda pid: ["grok-4.20"] if pid == "xai-oauth" else []) + try: + from api.providers import get_providers + import api.providers as providers + + monkeypatch.setattr(providers, "_read_live_provider_model_ids", lambda pid: ["grok-4.20"] if pid == "xai-oauth" else []) + + result = get_providers() + grok = next(p for p in result["providers"] if p["id"] == "xai-oauth") + assert grok["display_name"] == "xAI Grok OAuth" + assert grok["is_oauth"] is True + assert grok["configurable"] is False + assert grok["key_source"] == "oauth" + assert grok["models"] == [{"id": "grok-4.20", "label": "Grok 4.20"}] + assert grok["models_total"] == 1 + finally: + restore() + + +def test_xai_oauth_model_picker_group_uses_live_catalog(monkeypatch, tmp_path): + restore = _with_config( + monkeypatch, + tmp_path, + { + "model": {"provider": "xai-oauth", "default": "grok-4.20"}, + "providers": {"xai-oauth": {}}, + }, + ) + monkeypatch.setattr(config, "_read_live_provider_model_ids", lambda pid: ["grok-4.20"] if pid == "xai-oauth" else []) + try: + result = config.get_available_models() + group = next(g for g in result["groups"] if g["provider_id"] == "xai-oauth") + assert group["provider"] == "xAI Grok OAuth" + assert group["models"] == [{"id": "grok-4.20", "label": "Grok 4.20"}] + assert result["active_provider"] == "xai-oauth" + finally: + restore()