mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-29 18:46:59 +00:00
fix(hermes_cli): keep TTS/setup xAI OAuth from switching active chat provider
Save side-tool OAuth tokens without promoting xai-oauth via active_provider or model.provider so hermes setup tts login no longer hijacks inference routing.
This commit is contained in:
parent
a54067a6fd
commit
fce06e909d
5 changed files with 134 additions and 6 deletions
|
|
@ -4452,7 +4452,15 @@ def _save_xai_oauth_tokens(
|
|||
redirect_uri: str = "",
|
||||
last_refresh: Optional[str] = None,
|
||||
auth_mode: str = "oauth_device_code",
|
||||
set_active: bool = True,
|
||||
) -> None:
|
||||
"""Persist xAI OAuth tokens into the auth store.
|
||||
|
||||
When *set_active* is True (default), also promote ``xai-oauth`` to
|
||||
``active_provider`` — appropriate for intentional model/auth login.
|
||||
Pass ``set_active=False`` for side-tool credential bootstrap (TTS, STT,
|
||||
dashboard token save, token refresh) so inference routing is unchanged.
|
||||
"""
|
||||
if last_refresh is None:
|
||||
last_refresh = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
with _auth_store_lock():
|
||||
|
|
@ -4470,7 +4478,9 @@ def _save_xai_oauth_tokens(
|
|||
state["discovery"] = discovery
|
||||
if redirect_uri:
|
||||
state["redirect_uri"] = redirect_uri
|
||||
_save_provider_state(auth_store, "xai-oauth", state)
|
||||
_store_provider_state(
|
||||
auth_store, "xai-oauth", state, set_active=set_active
|
||||
)
|
||||
_save_auth_store(auth_store)
|
||||
if write_through_to_root:
|
||||
_write_through_xai_oauth_to_global_root(state)
|
||||
|
|
@ -4808,6 +4818,9 @@ def _refresh_xai_oauth_tokens(
|
|||
redirect_uri=redirect_uri,
|
||||
last_refresh=refreshed["last_refresh"],
|
||||
auth_mode=auth_mode,
|
||||
# Refresh must not flip active_provider — TTS/side tools can refresh
|
||||
# xAI tokens while chat still routes through another provider.
|
||||
set_active=False,
|
||||
)
|
||||
return updated_tokens
|
||||
|
||||
|
|
|
|||
|
|
@ -896,15 +896,17 @@ def _xai_oauth_logged_in_for_setup() -> bool:
|
|||
def _run_xai_oauth_login_from_setup() -> bool:
|
||||
"""Run the xAI Grok OAuth device-code login from inside the setup wizard.
|
||||
|
||||
Saves OAuth tokens only. Does **not** switch the active inference
|
||||
provider or rewrite ``model.provider`` — callers (TTS setup, tools
|
||||
config) only need credentials for side tools.
|
||||
|
||||
Returns True on success, False on any failure (the caller falls back
|
||||
to whatever the user picked next, e.g. Edge TTS).
|
||||
"""
|
||||
try:
|
||||
from hermes_cli.auth import (
|
||||
DEFAULT_XAI_OAUTH_BASE_URL,
|
||||
_is_remote_session,
|
||||
_save_xai_oauth_tokens,
|
||||
_update_config_for_provider,
|
||||
_xai_oauth_device_code_login,
|
||||
)
|
||||
except Exception as exc:
|
||||
|
|
@ -922,9 +924,7 @@ def _run_xai_oauth_login_from_setup() -> bool:
|
|||
redirect_uri=creds.get("redirect_uri", ""),
|
||||
last_refresh=creds.get("last_refresh"),
|
||||
auth_mode="oauth_device_code",
|
||||
)
|
||||
_update_config_for_provider(
|
||||
"xai-oauth", creds.get("base_url", DEFAULT_XAI_OAUTH_BASE_URL)
|
||||
set_active=False,
|
||||
)
|
||||
return True
|
||||
except Exception as exc:
|
||||
|
|
|
|||
|
|
@ -11153,6 +11153,9 @@ def _xai_device_poller(session_id: str) -> None:
|
|||
discovery=discovery,
|
||||
last_refresh=datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
|
||||
auth_mode="oauth_device_code",
|
||||
# Dashboard OAuth only bootstraps credentials for side tools;
|
||||
# do not hijack the active chat inference provider.
|
||||
set_active=False,
|
||||
)
|
||||
# The singleton write above is the single source of truth: the
|
||||
# credential-pool load seeds it as the canonical ``device_code``
|
||||
|
|
|
|||
|
|
@ -281,6 +281,40 @@ def test_save_and_read_xai_oauth_tokens_roundtrip(tmp_path, monkeypatch):
|
|||
assert data["discovery"]["token_endpoint"] == "https://auth.x.ai/oauth2/token"
|
||||
|
||||
|
||||
def test_save_xai_oauth_tokens_set_active_false_preserves_active_provider(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
"""Side-tool credential saves must not flip auth.json active_provider."""
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir(parents=True, exist_ok=True)
|
||||
auth_path = hermes_home / "auth.json"
|
||||
auth_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 1,
|
||||
"active_provider": "openrouter",
|
||||
"providers": {},
|
||||
}
|
||||
)
|
||||
)
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
|
||||
_save_xai_oauth_tokens(
|
||||
{
|
||||
"access_token": "side-tool-at",
|
||||
"refresh_token": "side-tool-rt",
|
||||
"id_token": "",
|
||||
"token_type": "Bearer",
|
||||
},
|
||||
discovery={"token_endpoint": "https://auth.x.ai/oauth2/token"},
|
||||
set_active=False,
|
||||
)
|
||||
|
||||
raw = json.loads(auth_path.read_text())
|
||||
assert raw["active_provider"] == "openrouter"
|
||||
assert raw["providers"]["xai-oauth"]["tokens"]["access_token"] == "side-tool-at"
|
||||
|
||||
|
||||
def test_read_xai_oauth_tokens_missing(tmp_path, monkeypatch):
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir(parents=True, exist_ok=True)
|
||||
|
|
|
|||
78
tests/hermes_cli/test_setup_tts_xai_oauth.py
Normal file
78
tests/hermes_cli/test_setup_tts_xai_oauth.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""Regression: TTS/setup xAI OAuth must not hijack the active chat provider."""
|
||||
|
||||
import json
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def test_run_xai_oauth_login_from_setup_does_not_hijack_active_provider(
|
||||
tmp_path, monkeypatch
|
||||
):
|
||||
"""TTS/setup OAuth must save tokens without switching chat inference routing.
|
||||
|
||||
Regression: `_run_xai_oauth_login_from_setup` used to call
|
||||
`_update_config_for_provider("xai-oauth")` (and token save flipped
|
||||
`active_provider`), so `hermes setup tts` OAuth login hijacked the main
|
||||
chat provider.
|
||||
"""
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir(parents=True, exist_ok=True)
|
||||
monkeypatch.setenv("HERMES_HOME", str(hermes_home))
|
||||
|
||||
auth_path = hermes_home / "auth.json"
|
||||
auth_path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"version": 1,
|
||||
"active_provider": "openrouter",
|
||||
"providers": {},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
config_path = hermes_home / "config.yaml"
|
||||
config_path.write_text(
|
||||
yaml.safe_dump(
|
||||
{
|
||||
"model": {
|
||||
"provider": "openrouter",
|
||||
"default": "anthropic/claude-sonnet-4",
|
||||
"base_url": "https://openrouter.ai/api/v1",
|
||||
},
|
||||
"tts": {"provider": "edge"},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"hermes_cli.auth._xai_oauth_device_code_login",
|
||||
lambda **kwargs: {
|
||||
"tokens": {
|
||||
"access_token": "tts-xai-access",
|
||||
"refresh_token": "tts-xai-refresh",
|
||||
"id_token": "",
|
||||
"token_type": "Bearer",
|
||||
},
|
||||
"discovery": {"token_endpoint": "https://auth.x.ai/oauth2/token"},
|
||||
"redirect_uri": "",
|
||||
"base_url": "https://api.x.ai/v1",
|
||||
"last_refresh": "2026-07-25T12:00:00Z",
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr("hermes_cli.auth._is_remote_session", lambda: True)
|
||||
|
||||
from hermes_cli.setup import _run_xai_oauth_login_from_setup
|
||||
|
||||
assert _run_xai_oauth_login_from_setup() is True
|
||||
|
||||
auth = json.loads(auth_path.read_text(encoding="utf-8"))
|
||||
assert auth["active_provider"] == "openrouter"
|
||||
xai_state = auth["providers"]["xai-oauth"]
|
||||
assert xai_state["tokens"]["access_token"] == "tts-xai-access"
|
||||
assert xai_state["tokens"]["refresh_token"] == "tts-xai-refresh"
|
||||
|
||||
config = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
assert config["model"]["provider"] == "openrouter"
|
||||
assert config["model"]["base_url"] == "https://openrouter.ai/api/v1"
|
||||
assert config["model"]["default"] == "anthropic/claude-sonnet-4"
|
||||
Loading…
Add table
Add a link
Reference in a new issue