From 6975a2d9ae20c5131c4fd3b3758dc9eade8cc6a0 Mon Sep 17 00:00:00 2001 From: teknium1 <127238744+teknium1@users.noreply.github.com> Date: Sat, 16 May 2026 23:33:18 -0700 Subject: [PATCH] =?UTF-8?q?fix(xai-oauth):=20entitlement-403=20chain=20?= =?UTF-8?q?=E2=80=94=20final=20state=20(ce0e189d3=20+=209818b9a1a=20+=2067?= =?UTF-8?q?84c8079=20+=20dffb602f3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapses the four-commit xAI entitlement-403 chain to its final on-main state, ported to the post-refactor module layout: - Added _is_entitlement_failure on AIAgent (run_agent.py) — detects Grok subscription-shape 403s on (401|403|None) status codes. - Added entitlement-skip branch to recover_with_credential_pool (agent/agent_runtime_helpers.py) — breaks the refresh-loop that Don's 100-iteration trace exposed when a Premium+ user hit a real entitlement issue. - Removed _decorate_xai_entitlement_error and unwrapped its two _summarize_api_error call sites — xAI's own body text already points users at grok.com/?_s=usage so we surface that verbatim (dffb602f3 reasoning: X Premium subs DO now work per xAI's 2026-05-16 announcement, so editorialising would misdirect). - grok-4.3 1M context entry landed in agent/model_metadata.py via the prior merge — no additional port needed. Tests already on disk (tests/run_agent/test_codex_xai_oauth_recovery.py) assert _is_entitlement_failure shape and verbatim body surfacing. Closes #27110. Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com> --- agent/agent_runtime_helpers.py | 9 +++++ run_agent.py | 69 +++++++++++++++++----------------- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/agent/agent_runtime_helpers.py b/agent/agent_runtime_helpers.py index 797047f95d3..ea48163ba0b 100644 --- a/agent/agent_runtime_helpers.py +++ b/agent/agent_runtime_helpers.py @@ -598,6 +598,15 @@ def recover_with_credential_pool( return False, True if effective_reason == FailoverReason.auth: + if agent._is_entitlement_failure(error_context, status_code): + _ra().logger.info( + "Credential %s — entitlement-shaped 403 from %s; " + "skipping pool refresh (account lacks subscription, " + "not a transient auth failure).", + status_code if status_code is not None else "auth", + agent.provider or "provider", + ) + return False, has_retried_429 refreshed = pool.try_refresh_current() if refreshed is not None: _ra().logger.info(f"Credential auth failure — refreshed pool entry {getattr(refreshed, 'id', '?')}") diff --git a/run_agent.py b/run_agent.py index 80577a19be3..1cb0ae761e6 100644 --- a/run_agent.py +++ b/run_agent.py @@ -1288,43 +1288,42 @@ class AIAgent: _save_trajectory_to_file(trajectory, self.model, completed) @staticmethod - def _decorate_xai_entitlement_error(detail: str) -> str: - """Append a friendly hint when xAI's OAuth surface returns an - entitlement-shaped error. + def _is_entitlement_failure( + error_context: Optional[Dict[str, Any]], + status_code: Optional[int], + ) -> bool: + """Detect subscription/entitlement 403s that masquerade as auth failures. - xAI's ``/v1/responses`` endpoint replies to OAuth tokens that lack a - SuperGrok / X Premium subscription with HTTP 403 carrying a body like:: + Returned True only when the body text matches a known entitlement + shape AND the status is 401/403. Refreshing an OAuth token cannot + fix an unsubscribed account, so callers should surface the error + instead of looping the credential pool. - {"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/..."} + Current matches: + * xAI OAuth: "do not have an active Grok subscription" / + "out of available resources" / "does not have permission" + "grok" - The raw text is useful but the action the user needs to take (subscribe - on grok.com, or switch providers with ``/model``) isn't obvious from - the wire format. Detect the entitlement shape and append a hint. - - Matched once per detail string — won't double-decorate if the upstream - already concatenated the same text. + Extend here for new providers as we discover them (Anthropic's + Claude Max OAuth entitlement errors look distinct enough today that + the existing 1M-context-beta branch handles them; revisit if other + subscription tiers start producing the same loop signature). """ - 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 OAuth account lacks SuperGrok / X Premium entitlement for " - "this model. Subscribe at https://grok.com or run `/model` to " - "switch providers." - ) - if hint.strip() in detail: - return detail - return f"{detail}{hint}" + if status_code not in (401, 403, None): + return False + if not isinstance(error_context, dict): + return False + message = str(error_context.get("message") or "").lower() + reason = str(error_context.get("reason") or "").lower() + haystack = f"{message} {reason}" + if not haystack.strip(): + return False + if "do not have an active grok subscription" in haystack: + return True + if "out of available resources" in haystack and "grok" in haystack: + return True + if "does not have permission" in haystack and "grok" in haystack: + return True + return False @staticmethod def _summarize_api_error(error: Exception) -> str: @@ -1359,12 +1358,12 @@ class AIAgent: if msg: status_code = getattr(error, "status_code", None) 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 status_code = getattr(error, "status_code", None) 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]: if not key: