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