From 60ef36879299c925d2e2b7cb9ddfe9b99ed87605 Mon Sep 17 00:00:00 2001 From: xxxigm Date: Sat, 16 May 2026 16:19:57 +0700 Subject: [PATCH] fix(xai-oauth): split 403 (tier/entitlement) from 400/401 in token endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xAI's token endpoint returns HTTP 403 to the OAuth grant when the account isn't on the allowlist for API access (e.g. standard SuperGrok subscribers — see #26847). Treating it like a stale-token 400/401 made ``format_auth_error`` append "Run ``hermes model`` to re-authenticate", which is misleading because re-login can't change xAI's tier decision. Split 403 off in both ``refresh_xai_oauth_pure`` and the loopback login token exchange: * New error code ``xai_oauth_tier_denied`` with ``relogin_required=False`` * Message explains the entitlement gate and points at the ``XAI_API_KEY`` + ``provider: xai`` fallback * 400/401 still set ``relogin_required=True`` as before * 5xx still set ``relogin_required=False`` as before --- hermes_cli/auth.py | 43 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/hermes_cli/auth.py b/hermes_cli/auth.py index 021a537c023..8bab7674877 100644 --- a/hermes_cli/auth.py +++ b/hermes_cli/auth.py @@ -3443,12 +3443,34 @@ def refresh_xai_oauth_pure( ) if response.status_code != 200: detail = response.text.strip() + # ``403`` from xAI's token endpoint is almost always a tier / + # entitlement gate (the OAuth grant exists but the account isn't + # on the allowlist for API access). Re-running ``hermes model`` + # won't fix that — surface a separate error code so + # ``format_auth_error`` doesn't append a misleading + # re-authenticate hint, and point users at the ``XAI_API_KEY`` + # fallback. See #26847. + if response.status_code == 403: + raise AuthError( + "xAI token refresh failed with HTTP 403." + + (f" Response: {detail}" if detail else "") + + " This OAuth account is not authorized for xAI API" + " access — xAI may be restricting API/OAuth use to" + " specific SuperGrok tiers despite the in-app" + " subscription being active. Re-logging in won't" + " change that; set ``XAI_API_KEY`` and switch to" + " ``provider: xai`` (API-key path) if available, or" + " upgrade your subscription at https://x.ai/grok.", + provider="xai-oauth", + code="xai_oauth_tier_denied", + relogin_required=False, + ) raise AuthError( "xAI token refresh failed." + (f" Response: {detail}" if detail else ""), provider="xai-oauth", code="xai_refresh_failed", - relogin_required=(response.status_code in {400, 401, 403}), + relogin_required=(response.status_code in {400, 401}), ) try: payload = response.json() @@ -6225,6 +6247,25 @@ def _xai_oauth_exchange_code_for_tokens( if response.status_code != 200: body = response.text.strip() + # See ``refresh_xai_oauth_pure`` — token-exchange 403 also + # surfaces tier/entitlement gating from xAI's backend. Avoid + # the misleading "re-authenticate" hint and point at the API + # key fallback. See #26847. + if response.status_code == 403: + raise AuthError( + f"xAI token exchange failed (HTTP 403)." + + (f" Response: {body}" if body else "") + + " This OAuth account is not authorized for xAI API" + " access — xAI may be restricting API/OAuth use to" + " specific SuperGrok tiers despite the in-app" + " subscription being active. Set ``XAI_API_KEY``" + " and switch to ``provider: xai`` (API-key path) if" + " available, or upgrade your subscription at" + " https://x.ai/grok.", + provider="xai-oauth", + code="xai_oauth_tier_denied", + relogin_required=False, + ) raise AuthError( f"xAI token exchange failed (HTTP {response.status_code})." + (f" Response: {body}" if body else ""),