mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-24 05:41:40 +00:00
fix(xai): drop stale X Premium+ hint from entitlement 403 surfacing (#27110)
xAI announced on 2026-05-16 (https://x.ai/news/grok-hermes) that X Premium subscriptions now work in Hermes Agent. The hint we shipped in PR #26644 asserted the opposite ("X Premium+ does NOT include xAI API access — only standalone SuperGrok subscribers can use this provider"), which would now misdirect Premium+ users who hit any other 403 (no Grok sub at all, wrong tier, exhausted quota) into thinking they need to switch subscriptions when their sub is in fact valid. Remove _decorate_xai_entitlement_error and its two call sites in _summarize_api_error. xAI's own body text already says "Manage subscriptions at https://grok.com/?_s=usage" — surface that verbatim and let xAI's wording do the diagnosis. The _is_entitlement_failure guard (which prevents credential-pool refresh loops on entitlement 403s) and the reasoning-replay gating for xai-oauth are unrelated and untouched. Update tests to assert the body still surfaces verbatim and that no Hermes-side editorializing is appended.
This commit is contained in:
parent
fb05f5d4b5
commit
dffb602f37
2 changed files with 24 additions and 118 deletions
61
run_agent.py
61
run_agent.py
|
|
@ -5046,63 +5046,6 @@ class AIAgent:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _decorate_xai_entitlement_error(detail: str) -> str:
|
|
||||||
"""Append a neutral hint when xAI's OAuth surface returns the
|
|
||||||
permission-denied 403.
|
|
||||||
|
|
||||||
xAI's ``/v1/responses`` endpoint replies to several distinct failure
|
|
||||||
modes with the SAME body::
|
|
||||||
|
|
||||||
{"code": "The caller does not have permission to execute the
|
|
||||||
specified operation", "error": "You have either run out of
|
|
||||||
available resources or do not have an active Grok subscription.
|
|
||||||
Manage subscriptions at https://grok.com/?_s=usage or subscribe
|
|
||||||
at https://grok.com/supergrok"}
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
if not detail:
|
|
||||||
return detail
|
|
||||||
lower = detail.lower()
|
|
||||||
is_entitlement = (
|
|
||||||
"do not have an active grok subscription" in lower
|
|
||||||
or ("out of available resources" in lower and "grok" in lower)
|
|
||||||
or ("does not have permission" in lower and "grok" in lower)
|
|
||||||
)
|
|
||||||
if not is_entitlement:
|
|
||||||
return detail
|
|
||||||
hint = (
|
|
||||||
" — 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 "X Premium+ does NOT include" in detail:
|
|
||||||
return detail
|
|
||||||
return f"{detail}{hint}"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _summarize_api_error(error: Exception) -> str:
|
def _summarize_api_error(error: Exception) -> str:
|
||||||
"""Extract a human-readable one-liner from an API error.
|
"""Extract a human-readable one-liner from an API error.
|
||||||
|
|
@ -5142,12 +5085,12 @@ class AIAgent:
|
||||||
if msg:
|
if msg:
|
||||||
status_code = getattr(error, "status_code", None)
|
status_code = getattr(error, "status_code", None)
|
||||||
prefix = f"HTTP {status_code}: " if status_code else ""
|
prefix = f"HTTP {status_code}: " if status_code else ""
|
||||||
return AIAgent._decorate_xai_entitlement_error(f"{prefix}{msg[:300]}")
|
return f"{prefix}{msg[:300]}"
|
||||||
|
|
||||||
# Fallback: truncate the raw string but give more room than 200 chars
|
# Fallback: truncate the raw string but give more room than 200 chars
|
||||||
status_code = getattr(error, "status_code", None)
|
status_code = getattr(error, "status_code", None)
|
||||||
prefix = f"HTTP {status_code}: " if status_code else ""
|
prefix = f"HTTP {status_code}: " if status_code else ""
|
||||||
return AIAgent._decorate_xai_entitlement_error(f"{prefix}{raw[:500]}")
|
return f"{prefix}{raw[:500]}"
|
||||||
|
|
||||||
def _mask_api_key_for_logs(self, key: Optional[str]) -> Optional[str]:
|
def _mask_api_key_for_logs(self, key: Optional[str]) -> Optional[str]:
|
||||||
if not key:
|
if not key:
|
||||||
|
|
|
||||||
|
|
@ -158,19 +158,22 @@ def test_codex_stream_postlude_error_still_falls_back():
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Fix B: friendly entitlement message
|
# Fix B: surface xAI's entitlement body verbatim (no editorializing)
|
||||||
|
#
|
||||||
|
# The original PR #26644 appended a hint that led with "X Premium+ does NOT
|
||||||
|
# include xAI API access — only standalone SuperGrok subscribers can use this
|
||||||
|
# provider." xAI announced on 2026-05-16 that X Premium subs now work in
|
||||||
|
# Hermes (https://x.ai/news/grok-hermes), making that hint actively wrong:
|
||||||
|
# a Premium+ user hitting a real entitlement issue (no Grok sub, wrong tier,
|
||||||
|
# exhausted quota) would be misdirected to switch subscriptions when their
|
||||||
|
# Premium sub is in fact valid. We now surface xAI's own body text verbatim
|
||||||
|
# (which already says "Manage subscriptions at https://grok.com/?_s=usage")
|
||||||
|
# and leave the diagnosis to xAI's wording.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
def test_summarize_api_error_decorates_xai_entitlement_403():
|
def test_summarize_api_error_surfaces_xai_entitlement_body_verbatim():
|
||||||
"""xAI's OAuth 403 must surface the X Premium+ gotcha + neutral causes.
|
"""xAI's OAuth 403 body must surface as-is, with no Hermes-side hint."""
|
||||||
|
|
||||||
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
|
from run_agent import AIAgent
|
||||||
|
|
||||||
error = RuntimeError(
|
error = RuntimeError(
|
||||||
|
|
@ -180,45 +183,15 @@ def test_summarize_api_error_decorates_xai_entitlement_403():
|
||||||
"subscriptions at https://grok.com'}"
|
"subscriptions at https://grok.com'}"
|
||||||
)
|
)
|
||||||
summary = AIAgent._summarize_api_error(error)
|
summary = AIAgent._summarize_api_error(error)
|
||||||
# The original xAI text must survive — it's still useful diagnostic info.
|
# xAI's own body text must reach the user — they need it to diagnose.
|
||||||
assert "do not have an active Grok subscription" in summary
|
assert "do not have an active Grok subscription" in summary
|
||||||
# The hint MUST lead with the X Premium+ gotcha (most likely cause
|
# No stale claim that X Premium is incompatible with Hermes.
|
||||||
# for users who think they're subscribed).
|
assert "X Premium+ does NOT include" not in summary
|
||||||
assert "X Premium+ does NOT include" in summary
|
assert "standalone SuperGrok subscribers" not 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
|
|
||||||
|
|
||||||
|
|
||||||
def test_summarize_api_error_does_not_accuse_subscribers():
|
def test_summarize_api_error_xai_body_message_unwrapped():
|
||||||
"""Hint must not confidently say the user has no subscription.
|
"""SDK-style error with structured body surfaces the message cleanly."""
|
||||||
|
|
||||||
Don Piedro reported his subscription is active. The hint must not
|
|
||||||
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
|
|
||||||
|
|
||||||
error = RuntimeError(
|
|
||||||
"HTTP 403: do not have an active Grok subscription"
|
|
||||||
)
|
|
||||||
summary = AIAgent._summarize_api_error(error)
|
|
||||||
# MUST NOT contain language that flatly assumes the user is unsubscribed.
|
|
||||||
assert "lacks SuperGrok" not 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():
|
|
||||||
"""SDK-style error with structured body must also get the hint."""
|
|
||||||
from run_agent import AIAgent
|
from run_agent import AIAgent
|
||||||
|
|
||||||
class _XaiErr(Exception):
|
class _XaiErr(Exception):
|
||||||
|
|
@ -235,19 +208,9 @@ def test_summarize_api_error_decorates_xai_body_message():
|
||||||
|
|
||||||
summary = AIAgent._summarize_api_error(_XaiErr("403"))
|
summary = AIAgent._summarize_api_error(_XaiErr("403"))
|
||||||
assert "HTTP 403" in summary
|
assert "HTTP 403" in summary
|
||||||
assert "X Premium+ does NOT include" in summary
|
assert "do not have an active Grok subscription" in summary
|
||||||
|
# No editorializing on top of xAI's own wording.
|
||||||
|
assert "X Premium+ does NOT include" not in summary
|
||||||
def test_summarize_api_error_idempotent_for_entitlement_hint():
|
|
||||||
"""Decorating twice must not double up the hint."""
|
|
||||||
from run_agent import AIAgent
|
|
||||||
|
|
||||||
raw = "HTTP 403: do not have an active Grok subscription"
|
|
||||||
once = AIAgent._decorate_xai_entitlement_error(raw)
|
|
||||||
twice = AIAgent._decorate_xai_entitlement_error(once)
|
|
||||||
assert once == twice
|
|
||||||
# Sanity: the hint did fire on the first pass.
|
|
||||||
assert "X Premium+ does NOT include" in once
|
|
||||||
|
|
||||||
|
|
||||||
def test_summarize_api_error_passes_through_unrelated_errors():
|
def test_summarize_api_error_passes_through_unrelated_errors():
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue