Files
openswarm/backend/apps/agents/error_classify.py
T

100 lines
4.0 KiB
Python

import re
# Patterns that indicate an upstream transient problem (overload / rate limit /
# infra blip), safe to silently retry with backoff. Checked against the
# stringified exception from claude_agent_sdk / Claude CLI.
_TRANSIENT_CAPACITY_PATTERNS = re.compile(
r"(?:\b(?:429|500|502|503|504|529)\b"
r"|overloaded"
r"|service\s+(?:temporarily\s+)?unavailable"
r"|at\s+capacity"
r"|try\s+again\s+shortly"
r"|internal\s+server\s+error"
r"|rate[_\s-]?limit(?:_error)?"
r"|ECONNRESET|ETIMEDOUT|ENETUNREACH|fetch\s+failed"
r"|upstream\s+connect\s+error)",
re.IGNORECASE,
)
# Patterns that look rate-limit-ish but are actually non-transient (user quota,
# auth, context-window tier gate). Must NOT retry, upgrading, reauthing, or
# trimming context is required. The long-context-required variant is what
# Anthropic returns when an OAuth Pro/Max account ships a request whose input
# exceeds the 200K standard tier and would need the "extra usage" tier; the
# user can't recover by waiting, so we surface it instead of looping.
_NON_TRANSIENT_PATTERNS = re.compile(
r"(?:usage\s+cap\s+exceeded"
r"|reached\s+your\s+OpenSwarm.*plan\s+limit"
r"|no\s+active\s+subscription"
r"|subscription\s+(?:canceled|past_due)"
r"|invalid.*token"
r"|missing\s+bearer\s+token"
r"|extra\s+usage\s+is\s+required\s+for\s+long\s+context"
r"|long\s+context\s+(?:requests?\s+)?(?:requires?|not\s+(?:available|enabled))"
r"|401|403)",
re.IGNORECASE,
)
def _is_long_context_error(exc: BaseException, extra_text: str = "") -> bool:
"""True when the upstream error is the 'long context tier required' 429.
Used by the catch-all error path to emit a friendly context-overflow
event instead of a generic system-error message.
"""
combined = f"{exc!s}\n{extra_text}".strip()
if not combined:
return False
return bool(re.search(
r"extra\s+usage\s+is\s+required\s+for\s+long\s+context"
r"|long\s+context\s+(?:requests?\s+)?(?:requires?|not\s+(?:available|enabled))",
combined,
re.IGNORECASE,
))
def _is_auth_error(exc: BaseException, extra_text: str = "") -> bool:
"""True when the upstream error is a 401/403 auth failure.
Used by the catch-all error path to surface a friendly "subscription
expired / reconnect" card instead of dumping the raw 401 JSON. The most
common cause: the OpenSwarm Pro bearer or 9Router OAuth token has expired
while the UI still shows the connection as 'connected'.
"""
combined = f"{exc!s}\n{extra_text}".strip()
if not combined:
return False
return bool(re.search(
r"\b(401|403)\b"
r"|invalid\s+authentication\s+credentials"
r"|invalid.*api[_\s-]?key"
r"|missing\s+bearer\s+token"
r"|unauthori[sz]ed"
r"|no\s+credentials\s+for\s+provider"
r"|provider\s+not\s+(?:configured|connected|authorized)",
combined,
re.IGNORECASE,
))
def _is_transient_capacity_error(exc: BaseException, extra_text: str = "") -> bool:
# The Claude CLI's underlying ProcessError stringifies to a generic
# "Command failed with exit code 1 / Check stderr output for details";
# the real cause (rate_limit_error / No pool capacity available / 429
# / overloaded) only surfaces in the subprocess's stderr stream, which
# we capture via the SDK's `stderr` callback and pass in as extra_text.
# Classify against both so we catch capacity errors regardless of which
# channel carried the message.
combined = f"{exc!s}\n{extra_text}".strip()
if not combined:
return False
if _NON_TRANSIENT_PATTERNS.search(combined):
return False
if _TRANSIENT_CAPACITY_PATTERNS.search(combined):
return True
# Pool-exhaustion copy from the OpenSwarm proxy ("No pool capacity
# available. Try again shortly."), matches the capacity family too.
if re.search(r"no\s+pool\s+capacity", combined, re.IGNORECASE):
return True
return False