From cd68b8f0e8f486a4a5ceaeda41d440ba3342d077 Mon Sep 17 00:00:00 2001 From: AhmetArif0 <147827411+AhmetArif0@users.noreply.github.com> Date: Tue, 2 Jun 2026 22:31:22 +0300 Subject: [PATCH] fix(auth): set active_provider after hermes auth add qwen-oauth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hermes auth add qwen-oauth called pool.add_entry() but never wrote to providers["qwen-oauth"] or set active_provider in auth.json. _model_section_has_credentials() checks get_active_provider() first; with active_provider unset and no api_key_env_vars configured for oauth_external providers, the setup wizard reported "No inference provider configured" even after a successful Qwen CLI OAuth login. Add _mark_qwen_oauth_active() in auth.py: writes a minimal provider state entry (base_url for display only) and calls _save_provider_state() to set active_provider. The function deliberately does not copy the api_key — that lives in the Qwen CLI credential file managed by _save_qwen_cli_tokens / resolve_qwen_runtime_credentials and must not be duplicated in auth.json where it would become stale. pool.add_entry() is retained so "hermes auth list" continues to show the entry. Runtime credential resolution continues to use resolve_qwen_runtime_credentials. Mirrors the fix applied to openai-codex (#37517) and xai-oauth (#37576). --- hermes_cli/auth.py | 19 ++++++++++ hermes_cli/auth_commands.py | 1 + tests/hermes_cli/test_auth_commands.py | 49 ++++++++++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 929d2de4c..021905c3e 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -2037,6 +2037,25 @@ def _refresh_qwen_cli_tokens(tokens: Dict[str, Any], timeout_seconds: float = 20 return refreshed +def _mark_qwen_oauth_active(creds: Dict[str, Any]) -> None: + """Set active_provider to qwen-oauth in auth.json. + + Qwen OAuth tokens live in the Qwen CLI credential file managed by + _save_qwen_cli_tokens / resolve_qwen_runtime_credentials. This function + only writes a minimal provider-state entry (base_url for display) and + sets active_provider so that get_active_provider() and + _model_section_has_credentials() detect the provider for the setup wizard + and status commands. + """ + with _auth_store_lock(): + auth_store = _load_auth_store() + state: Dict[str, Any] = {} + if creds.get("base_url"): + state["base_url"] = str(creds["base_url"]) + _save_provider_state(auth_store, "qwen-oauth", state) + _save_auth_store(auth_store) + + def resolve_qwen_runtime_credentials( *, force_refresh: bool = False, diff --git a/hermes_cli/auth_commands.py b/hermes_cli/auth_commands.py index d692a5c1e..ff03e8440 100644 --- a/hermes_cli/auth_commands.py +++ b/hermes_cli/auth_commands.py @@ -367,6 +367,7 @@ def auth_add_command(args) -> None: if provider == "qwen-oauth": creds = auth_mod.resolve_qwen_runtime_credentials(refresh_if_expiring=False) + auth_mod._mark_qwen_oauth_active(creds) label = (getattr(args, "label", None) or "").strip() or label_from_token( creds["api_key"], _oauth_default_label(provider, len(pool.entries()) + 1), diff --git a/tests/hermes_cli/test_auth_commands.py b/tests/hermes_cli/test_auth_commands.py index 660ea368b..b53e73737 100644 --- a/tests/hermes_cli/test_auth_commands.py +++ b/tests/hermes_cli/test_auth_commands.py @@ -142,6 +142,55 @@ def test_auth_add_google_gemini_cli_sets_active_provider(tmp_path, monkeypatch): assert entry["access_token"] == "ya29.test-token" +def test_auth_add_qwen_oauth_sets_active_provider(tmp_path, monkeypatch): + """hermes auth add qwen-oauth must set active_provider in auth.json. + + Tokens are managed by the Qwen CLI credential file via + resolve_qwen_runtime_credentials(). The auth.json entry must record + active_provider — without storing tokens that would become stale. + """ + monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) + _write_auth_store(tmp_path, {"version": 1, "providers": {}}) + _fake_creds = { + "provider": "qwen-oauth", + "base_url": "https://portal.qwen.ai/v1", + "api_key": "qwen-test-token", + "source": "qwen-cli", + "expires_at_ms": None, + "auth_file": "/home/user/.qwen/oauth_creds.json", + } + monkeypatch.setattr( + "hermes_cli.auth.resolve_qwen_runtime_credentials", + lambda **kw: _fake_creds, + ) + # Prevent _seed_from_singletons from calling the real Qwen CLI file path + monkeypatch.setattr( + "agent.credential_pool._seed_from_singletons", + lambda provider, entries: (False, set()), + ) + + from hermes_cli.auth_commands import auth_add_command + + class _Args: + provider = "qwen-oauth" + auth_type = "oauth" + api_key = None + label = None + + auth_add_command(_Args()) + + payload = json.loads((tmp_path / "hermes" / "auth.json").read_text()) + assert payload["active_provider"] == "qwen-oauth" + state = payload["providers"]["qwen-oauth"] + # Only base_url stored — no api_key (that lives in the Qwen CLI file). + assert state.get("base_url") == "https://portal.qwen.ai/v1" + assert "api_key" not in state + # pool entry from pool.add_entry() still present for hermes auth list + entries = payload["credential_pool"]["qwen-oauth"] + entry = next(item for item in entries if item["source"] == "manual:qwen_cli") + assert entry["access_token"] == "qwen-test-token" + + def test_auth_add_nous_oauth_persists_pool_entry(tmp_path, monkeypatch): monkeypatch.setenv("HERMES_HOME", str(tmp_path / "hermes")) _write_auth_store(tmp_path, {"version": 1, "providers": {}})