Coverage for src / local_deep_research / web_search_engines / rate_limiting / llm / detection.py: 69%
43 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-01-11 00:51 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2026-01-11 00:51 +0000
1"""
2LLM-specific rate limit error detection.
3"""
5from loguru import logger
8def is_llm_rate_limit_error(error: Exception) -> bool:
9 """
10 Detect if an error is a rate limit error from an LLM provider.
12 Args:
13 error: The exception to check
15 Returns:
16 True if this is a rate limit error, False otherwise
17 """
18 error_str = str(error).lower()
19 error_type = type(error).__name__.lower()
21 # Check for explicit HTTP 429 status codes
22 if hasattr(error, "response") and hasattr(error.response, "status_code"):
23 if error.response.status_code == 429: 23 ↛ 28line 23 didn't jump to line 28 because the condition on line 23 was always true
24 logger.debug(f"Detected HTTP 429 rate limit error: {error}")
25 return True
27 # Check for common rate limit error messages
28 rate_limit_indicators = [
29 "rate limit",
30 "rate_limit",
31 "ratelimit",
32 "too many requests",
33 "quota exceeded",
34 "quota has been exhausted",
35 "resource has been exhausted",
36 "429",
37 "threshold",
38 "try again later",
39 "slow down",
40 ]
42 if any(indicator in error_str for indicator in rate_limit_indicators):
43 logger.debug(f"Detected rate limit error from message: {error}")
44 return True
46 # Check for specific provider error types
47 if "ratelimiterror" in error_type or "quotaexceeded" in error_type: 47 ↛ 48line 47 didn't jump to line 48 because the condition on line 47 was never true
48 logger.debug(f"Detected rate limit error from type: {type(error)}")
49 return True
51 # Check for OpenAI specific rate limit errors
52 if hasattr(error, "__class__") and error.__class__.__module__ == "openai": 52 ↛ 53line 52 didn't jump to line 53 because the condition on line 52 was never true
53 if error_type in ["ratelimiterror", "apierror"] and "429" in error_str:
54 logger.debug(f"Detected OpenAI rate limit error: {error}")
55 return True
57 # Check for Anthropic specific rate limit errors
58 if ( 58 ↛ 62line 58 didn't jump to line 62 because the condition on line 58 was never true
59 hasattr(error, "__class__")
60 and "anthropic" in error.__class__.__module__
61 ):
62 if any(x in error_str for x in ["rate_limit", "429", "too many"]):
63 logger.debug(f"Detected Anthropic rate limit error: {error}")
64 return True
66 return False
69def extract_retry_after(error: Exception) -> float:
70 """
71 Extract retry-after time from rate limit error if available.
73 Args:
74 error: The rate limit error
76 Returns:
77 Retry after time in seconds, or 0 if not found
78 """
79 # Check for Retry-After header in response
80 if hasattr(error, "response") and hasattr(error.response, "headers"):
81 retry_after = error.response.headers.get("Retry-After")
82 if retry_after: 82 ↛ 91line 82 didn't jump to line 91 because the condition on line 82 was always true
83 try:
84 return float(retry_after)
85 except ValueError:
86 logger.debug(
87 f"Could not parse Retry-After header: {retry_after}"
88 )
90 # Try to extract from error message
91 error_str = str(error)
93 # Look for patterns like "try again in X seconds"
94 import re
96 patterns = [
97 r"try again in (\d+(?:\.\d+)?)\s*seconds?",
98 r"retry after (\d+(?:\.\d+)?)\s*seconds?",
99 r"wait (\d+(?:\.\d+)?)\s*seconds?",
100 ]
102 for pattern in patterns:
103 match = re.search(pattern, error_str, re.IGNORECASE)
104 if match:
105 try:
106 return float(match.group(1))
107 except ValueError:
108 pass
110 return 0