diff --git a/run_agent.py b/run_agent.py index da05e7e8239..85c1128d68e 100644 --- a/run_agent.py +++ b/run_agent.py @@ -5018,20 +5018,21 @@ class AIAgent: Manage subscriptions at https://grok.com/?_s=usage or subscribe at https://grok.com/supergrok"} - That body covers at least four real causes we cannot distinguish - without more info from xAI: + That body covers several real causes we cannot distinguish without + more info from xAI. The most common (and least obvious) one is + that **X Premium+ does NOT include API access** — only standalone + SuperGrok subscribers can use Hermes against xai-oauth. Lots of + users see Grok in their X app, assume it works here too, and hit + this 403 with no idea why. Lead the hint with that. - * Account has no Grok subscription at all - * Account has SuperGrok but the tier doesn't include the requested - model (e.g. grok-4.3 needs SuperGrok Heavy) - * Monthly quota for the subscribed tier is exhausted (the - ``?_s=usage`` URL hints at this) - * SuperGrok is active but the API access add-on isn't enabled + Other possible causes: + * No Grok subscription at all + * SuperGrok tier doesn't include the requested model (e.g. + grok-4.3 may need a higher tier) + * Monthly quota exhausted (the ``?_s=usage`` URL hints at this) - Picking one ("you're not subscribed") is wrong for the other three - and reads as insulting to subscribers. Surface the raw xAI text - verbatim and point at https://grok.com/?_s=usage where the user - can see WHICH of those four it is. + Surface the raw xAI text verbatim and point at + https://grok.com/?_s=usage where the user can see WHICH applies. Matched once per detail string — won't double-decorate if the upstream already concatenated the same text. @@ -5047,15 +5048,16 @@ class AIAgent: if not is_entitlement: return detail hint = ( - " — xAI rejected the request on this OAuth account. Could be a " - "missing subscription, a tier that doesn't include this model, an " - "exhausted quota, or API access not enabled. Check " - "https://grok.com/?_s=usage to see which, or run `/model` to " - "switch providers." + " — xAI rejected this OAuth account. NOTE: X Premium+ does NOT " + "include xAI API access — only standalone SuperGrok subscribers " + "can use this provider. Other possible causes: no Grok " + "subscription, your tier doesn't include this model, or your " + "quota is exhausted. Check https://grok.com/?_s=usage to see " + "which, or run `/model` to switch providers." ) # Idempotency: detect prior decoration by a substring unique to the # hint (not present in xAI's own body text). - if "Could be a missing subscription" in detail: + if "X Premium+ does NOT include" in detail: return detail return f"{detail}{hint}" diff --git a/tests/run_agent/test_codex_xai_oauth_recovery.py b/tests/run_agent/test_codex_xai_oauth_recovery.py index c64f46eea09..9192d50695b 100644 --- a/tests/run_agent/test_codex_xai_oauth_recovery.py +++ b/tests/run_agent/test_codex_xai_oauth_recovery.py @@ -163,11 +163,13 @@ def test_codex_stream_postlude_error_still_falls_back(): def test_summarize_api_error_decorates_xai_entitlement_403(): - """xAI's OAuth 403 must end with the neutral 4-cause hint. + """xAI's OAuth 403 must surface the X Premium+ gotcha + neutral causes. - Wording is deliberately ambiguous because xAI returns the SAME body for: - no subscription, wrong tier, exhausted quota, or API access not enabled. - Picking one (e.g. "you're not subscribed") would insult subscribers. + Wording deliberately leads with the X Premium+ gotcha because that's + the #1 confusing case: people see Grok in their X app, assume it + works here too, and hit this 403 with no idea API access is a + separate SKU. Other causes (no subscription, wrong tier, exhausted + quota) follow. """ from run_agent import AIAgent @@ -180,13 +182,15 @@ def test_summarize_api_error_decorates_xai_entitlement_403(): summary = AIAgent._summarize_api_error(error) # The original xAI text must survive — it's still useful diagnostic info. assert "do not have an active Grok subscription" in summary - # The hint must NOT confidently assert "lacks subscription"; it must - # acknowledge the 4 possible causes. - assert "Could be a missing subscription" in summary - assert "tier that doesn't include this model" in summary - assert "exhausted quota" in summary - assert "API access not enabled" in summary - # The hint must point at the usage page where the user can verify which. + # The hint MUST lead with the X Premium+ gotcha (most likely cause + # for users who think they're subscribed). + assert "X Premium+ does NOT include" in summary + assert "standalone SuperGrok subscribers" in summary + # Other causes still listed. + assert "no Grok subscription" in summary + assert "tier doesn't include this model" in summary + assert "quota is exhausted" in summary + # The hint must point at the usage page where the user can verify. assert "https://grok.com/?_s=usage" in summary # Switching providers is still a valid escape hatch. assert "/model" in summary @@ -196,8 +200,9 @@ def test_summarize_api_error_does_not_accuse_subscribers(): """Hint must not confidently say the user has no subscription. Don Piedro reported his subscription is active. The hint must not - contradict him — it must list all 4 possible causes and let him - check which one applies. + contradict him — leading with the X Premium+ gotcha gives subscribers + a plausible reason ("oh, I'm on Premium+ not pure SuperGrok") instead + of accusing them of lying about having a subscription. """ from run_agent import AIAgent @@ -205,12 +210,11 @@ def test_summarize_api_error_does_not_accuse_subscribers(): "HTTP 403: do not have an active Grok subscription" ) summary = AIAgent._summarize_api_error(error) - # MUST NOT contain language that assumes the user is unsubscribed. + # MUST NOT contain language that flatly assumes the user is unsubscribed. assert "lacks SuperGrok" not in summary - assert "lacks subscription" not in summary - assert "your account doesn't have" not in summary.lower() - # MUST contain the neutral framing. - assert "Could be" in summary or "could be" in summary + assert "you are not subscribed" not in summary.lower() + # MUST lead with the most-likely-but-non-accusatory cause. + assert "X Premium+ does NOT include" in summary def test_summarize_api_error_decorates_xai_body_message(): @@ -231,7 +235,7 @@ def test_summarize_api_error_decorates_xai_body_message(): summary = AIAgent._summarize_api_error(_XaiErr("403")) assert "HTTP 403" in summary - assert "Could be a missing subscription" in summary + assert "X Premium+ does NOT include" in summary def test_summarize_api_error_idempotent_for_entitlement_hint(): @@ -243,7 +247,7 @@ def test_summarize_api_error_idempotent_for_entitlement_hint(): twice = AIAgent._decorate_xai_entitlement_error(once) assert once == twice # Sanity: the hint did fire on the first pass. - assert "Could be a missing subscription" in once + assert "X Premium+ does NOT include" in once def test_summarize_api_error_passes_through_unrelated_errors():