Files
autocoder/rate_limit_utils.py
cabana8471 f018b4c1d8 fix: address PR #109 review feedback from leonvanzyl
- BLOCKER: Remove clear_stuck_features import and psutil block (doesn't exist in upstream)
- Fix overly broad rate limit patterns to avoid false positives
  - Remove "please wait", "try again later", "limit reached", "429" (bare)
  - Convert to regex-based detection with word boundaries
  - Add patterns for "http 429", "status 429", "error 429"
- Add bounds checking (1-3600s) for parsed retry delays
- Use is_rate_limit_error() consistently instead of inline pattern matching
- Extract backoff functions to rate_limit_utils.py for testability
  - calculate_rate_limit_backoff() for exponential backoff
  - calculate_error_backoff() for linear backoff
  - clamp_retry_delay() for safe range enforcement
- Rename test_agent.py to test_rate_limit_utils.py (matches module)
- Add comprehensive false-positive tests:
  - Version numbers (v14.29.0)
  - Issue/PR numbers (#429)
  - Line numbers (file.py:429)
  - Port numbers (4293)
  - Legitimate wait/retry messages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 21:20:52 +01:00

121 lines
3.3 KiB
Python

"""
Rate Limit Utilities
====================
Shared utilities for detecting and handling API rate limits.
Used by both agent.py (production) and test_rate_limit_utils.py (tests).
"""
import re
from typing import Optional
# Regex patterns for rate limit detection (used in both exception messages and response text)
# These patterns use word boundaries to avoid false positives like "PR #429" or "please wait while I..."
RATE_LIMIT_REGEX_PATTERNS = [
r"\brate[_\s]?limit", # "rate limit", "rate_limit", "ratelimit"
r"\btoo\s+many\s+requests", # "too many requests"
r"\bhttp\s*429\b", # "http 429", "http429"
r"\bstatus\s*429\b", # "status 429", "status429"
r"\berror\s*429\b", # "error 429", "error429"
r"\b429\s+too\s+many", # "429 too many"
r"\boverloaded\b", # "overloaded"
r"\bquota\s*exceeded\b", # "quota exceeded"
]
# Compiled regex for efficient matching
_RATE_LIMIT_REGEX = re.compile(
"|".join(RATE_LIMIT_REGEX_PATTERNS),
re.IGNORECASE
)
def parse_retry_after(error_message: str) -> Optional[int]:
"""
Extract retry-after seconds from various error message formats.
Handles common formats:
- "Retry-After: 60"
- "retry after 60 seconds"
- "try again in 5 seconds"
- "30 seconds remaining"
Args:
error_message: The error message to parse
Returns:
Seconds to wait, or None if not parseable.
"""
patterns = [
r"retry.?after[:\s]+(\d+)\s*(?:seconds?)?",
r"try again in\s+(\d+)\s*(?:seconds?|s\b)",
r"(\d+)\s*seconds?\s*(?:remaining|left|until)",
]
for pattern in patterns:
match = re.search(pattern, error_message, re.IGNORECASE)
if match:
return int(match.group(1))
return None
def is_rate_limit_error(error_message: str) -> bool:
"""
Detect if an error message indicates a rate limit.
Uses regex patterns with word boundaries to avoid false positives
like "PR #429", "please wait while I...", or "Node v14.29.0".
Args:
error_message: The error message to check
Returns:
True if the message indicates a rate limit, False otherwise.
"""
return bool(_RATE_LIMIT_REGEX.search(error_message))
def calculate_rate_limit_backoff(retries: int) -> int:
"""
Calculate exponential backoff for rate limits.
Formula: min(60 * 2^retries, 3600) - caps at 1 hour
Sequence: 60s, 120s, 240s, 480s, 960s, 1920s, 3600s...
Args:
retries: Number of consecutive rate limit retries (0-indexed)
Returns:
Delay in seconds (clamped to 1-3600 range)
"""
return int(min(max(60 * (2 ** retries), 1), 3600))
def calculate_error_backoff(retries: int) -> int:
"""
Calculate linear backoff for non-rate-limit errors.
Formula: min(30 * retries, 300) - caps at 5 minutes
Sequence: 30s, 60s, 90s, 120s, ... 300s
Args:
retries: Number of consecutive error retries (1-indexed)
Returns:
Delay in seconds (clamped to 1-300 range)
"""
return min(max(30 * retries, 1), 300)
def clamp_retry_delay(delay_seconds: int) -> int:
"""
Clamp a retry delay to a safe range (1-3600 seconds).
Args:
delay_seconds: The raw delay value
Returns:
Delay clamped to 1-3600 seconds
"""
return min(max(delay_seconds, 1), 3600)