Spaces:
Paused
Paused
Commit ·
4e293c6
1
Parent(s): 6b2240a
feat: 实现反检测速率限制器,模拟人类行为模式避免被 Google 封禁
Browse files- Dockerfile +13 -5
- app/server/rate_limiter.py +188 -38
Dockerfile
CHANGED
|
@@ -44,11 +44,19 @@ ENV GEMINI_COOKIE_PATH="/home/user/src/cache"
|
|
| 44 |
# 设置图片存储路径(持久化生成的图片)
|
| 45 |
ENV GEMINI_IMAGE_STORE_PATH="/home/user/src/cache"
|
| 46 |
|
| 47 |
-
#
|
| 48 |
-
#
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
# 启动命令
|
| 54 |
# 确保 run.py 里的 uvicorn 监听的是 0.0.0.0 和 7860 端口
|
|
|
|
| 44 |
# 设置图片存储路径(持久化生成的图片)
|
| 45 |
ENV GEMINI_IMAGE_STORE_PATH="/home/user/src/cache"
|
| 46 |
|
| 47 |
+
# 反检测速率限制配置(重要:避免被 Google 封禁)
|
| 48 |
+
# 安全值(推荐):
|
| 49 |
+
# - max_concurrent: 1-5(模拟人类行为)
|
| 50 |
+
# - requests_per_minute: 10-30
|
| 51 |
+
# - requests_per_hour: 100-300
|
| 52 |
+
# - requests_per_day: 1000-3000
|
| 53 |
+
ENV GEMINI_MAX_CONCURRENT_REQUESTS="3"
|
| 54 |
+
ENV GEMINI_QUEUE_TIMEOUT="60.0"
|
| 55 |
+
ENV GEMINI_MAX_QUEUE_SIZE="50"
|
| 56 |
+
ENV GEMINI_REQUESTS_PER_MINUTE="20"
|
| 57 |
+
ENV GEMINI_REQUESTS_PER_HOUR="200"
|
| 58 |
+
ENV GEMINI_REQUESTS_PER_DAY="2000"
|
| 59 |
+
ENV GEMINI_BURST_COOLDOWN="30.0"
|
| 60 |
|
| 61 |
# 启动命令
|
| 62 |
# 确保 run.py 里的 uvicorn 监听的是 0.0.0.0 和 7860 端口
|
app/server/rate_limiter.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
-
|
| 4 |
"""
|
| 5 |
|
| 6 |
import asyncio
|
|
|
|
| 7 |
import time
|
| 8 |
from typing import Callable, Optional
|
| 9 |
|
|
@@ -13,79 +14,183 @@ from loguru import logger
|
|
| 13 |
|
| 14 |
class RateLimiter:
|
| 15 |
"""
|
| 16 |
-
|
| 17 |
|
| 18 |
-
|
| 19 |
-
-
|
| 20 |
-
-
|
| 21 |
-
-
|
| 22 |
-
-
|
|
|
|
| 23 |
"""
|
| 24 |
|
| 25 |
def __init__(
|
| 26 |
self,
|
| 27 |
-
max_concurrent: int =
|
| 28 |
-
base_timeout: float =
|
| 29 |
-
max_queue_size: int =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
):
|
| 31 |
"""
|
| 32 |
-
Initialize rate limiter.
|
| 33 |
|
| 34 |
Args:
|
| 35 |
-
max_concurrent:
|
| 36 |
-
base_timeout: Base timeout in seconds
|
| 37 |
-
max_queue_size: Maximum queued requests
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
"""
|
| 39 |
self.max_concurrent = max_concurrent
|
| 40 |
self.base_timeout = base_timeout
|
| 41 |
self.max_queue_size = max_queue_size
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
self._semaphore = asyncio.Semaphore(max_concurrent)
|
| 43 |
self._current_count = 0
|
| 44 |
self._queued_count = 0
|
| 45 |
self._lock = asyncio.Lock()
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
# Metrics
|
| 48 |
self._total_requests = 0
|
| 49 |
self._rejected_requests = 0
|
| 50 |
self._last_reset = time.time()
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
async def acquire(self, request: Optional[Request] = None) -> None:
|
| 53 |
"""
|
| 54 |
-
Acquire permission
|
| 55 |
|
| 56 |
Args:
|
| 57 |
-
request: Optional FastAPI request for context
|
| 58 |
|
| 59 |
Raises:
|
| 60 |
-
HTTPException: 503 when
|
| 61 |
"""
|
|
|
|
|
|
|
| 62 |
async with self._lock:
|
| 63 |
self._total_requests += 1
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
# Fast rejection if queue is full
|
| 66 |
if self._queued_count >= self.max_queue_size:
|
| 67 |
self._rejected_requests += 1
|
|
|
|
| 68 |
logger.warning(
|
| 69 |
f"Rate limiter: queue full ({self._queued_count}/{self.max_queue_size}), "
|
| 70 |
-
f"rejecting
|
| 71 |
)
|
| 72 |
raise HTTPException(
|
| 73 |
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
| 74 |
-
detail="Server is at capacity. Please try again
|
| 75 |
-
headers={"Retry-After":
|
| 76 |
)
|
| 77 |
|
| 78 |
self._queued_count += 1
|
| 79 |
current_queue_position = self._queued_count
|
| 80 |
|
| 81 |
-
#
|
| 82 |
-
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
adaptive_timeout = min(estimated_wait, self.base_timeout)
|
| 85 |
|
| 86 |
try:
|
| 87 |
logger.debug(
|
| 88 |
-
f"Rate limiter: request queued
|
| 89 |
f"timeout={adaptive_timeout:.1f}s"
|
| 90 |
)
|
| 91 |
|
|
@@ -97,9 +202,13 @@ class RateLimiter:
|
|
| 97 |
async with self._lock:
|
| 98 |
self._queued_count -= 1
|
| 99 |
self._current_count += 1
|
|
|
|
| 100 |
logger.info(
|
| 101 |
f"Rate limiter: acquired slot ({self._current_count}/{self.max_concurrent}), "
|
| 102 |
-
f"queue={self._queued_count}"
|
|
|
|
|
|
|
|
|
|
| 103 |
)
|
| 104 |
|
| 105 |
except asyncio.TimeoutError:
|
|
@@ -107,16 +216,15 @@ class RateLimiter:
|
|
| 107 |
self._queued_count -= 1
|
| 108 |
self._rejected_requests += 1
|
| 109 |
|
| 110 |
-
retry_after =
|
| 111 |
logger.warning(
|
| 112 |
-
f"Rate limiter: request timed out after {adaptive_timeout:.1f}s "
|
| 113 |
-
f"(queue was at position {current_queue_position}), "
|
| 114 |
f"suggesting retry after {retry_after}s"
|
| 115 |
)
|
| 116 |
|
| 117 |
raise HTTPException(
|
| 118 |
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
| 119 |
-
detail=
|
| 120 |
headers={"Retry-After": str(retry_after)},
|
| 121 |
)
|
| 122 |
|
|
@@ -130,12 +238,38 @@ class RateLimiter:
|
|
| 130 |
logger.debug(f"Rate limiter: released slot ({current_count}/{self.max_concurrent})")
|
| 131 |
|
| 132 |
def get_metrics(self) -> dict:
|
| 133 |
-
"""Get current rate limiter metrics."""
|
|
|
|
|
|
|
|
|
|
| 134 |
return {
|
|
|
|
| 135 |
"current_requests": self._current_count,
|
| 136 |
"queued_requests": self._queued_count,
|
|
|
|
|
|
|
| 137 |
"max_concurrent": self.max_concurrent,
|
| 138 |
"max_queue_size": self.max_queue_size,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
"total_requests": self._total_requests,
|
| 140 |
"rejected_requests": self._rejected_requests,
|
| 141 |
"rejection_rate": (
|
|
@@ -143,7 +277,7 @@ class RateLimiter:
|
|
| 143 |
if self._total_requests > 0
|
| 144 |
else 0.0
|
| 145 |
),
|
| 146 |
-
"uptime_seconds":
|
| 147 |
}
|
| 148 |
|
| 149 |
def reset_metrics(self) -> None:
|
|
@@ -158,24 +292,40 @@ _rate_limiter: RateLimiter | None = None
|
|
| 158 |
|
| 159 |
|
| 160 |
def get_rate_limiter() -> RateLimiter:
|
| 161 |
-
"""Get or create the global rate limiter."""
|
| 162 |
global _rate_limiter
|
| 163 |
if _rate_limiter is None:
|
| 164 |
# Configure based on environment or use defaults
|
|
|
|
| 165 |
import os
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
logger.info(
|
| 171 |
-
f"Rate limiter initialized
|
| 172 |
-
f"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
)
|
| 174 |
|
| 175 |
_rate_limiter = RateLimiter(
|
| 176 |
max_concurrent=max_concurrent,
|
| 177 |
base_timeout=base_timeout,
|
| 178 |
max_queue_size=max_queue_size,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
)
|
| 180 |
return _rate_limiter
|
| 181 |
|
|
|
|
| 1 |
"""
|
| 2 |
+
Anti-detection rate limiter for Gemini Web API.
|
| 3 |
+
Mimics human browsing patterns to avoid detection by Google.
|
| 4 |
"""
|
| 5 |
|
| 6 |
import asyncio
|
| 7 |
+
import random
|
| 8 |
import time
|
| 9 |
from typing import Callable, Optional
|
| 10 |
|
|
|
|
| 14 |
|
| 15 |
class RateLimiter:
|
| 16 |
"""
|
| 17 |
+
Human-like rate limiter with anti-detection features.
|
| 18 |
|
| 19 |
+
Anti-detection strategies:
|
| 20 |
+
- Low concurrent limit (1-3 to mimic human behavior)
|
| 21 |
+
- Randomized request delays (jitter)
|
| 22 |
+
- Per-client rate limiting
|
| 23 |
+
- Daily quota per account
|
| 24 |
+
- Cooldown periods after burst usage
|
| 25 |
"""
|
| 26 |
|
| 27 |
def __init__(
|
| 28 |
self,
|
| 29 |
+
max_concurrent: int = 3,
|
| 30 |
+
base_timeout: float = 60.0,
|
| 31 |
+
max_queue_size: int = 50,
|
| 32 |
+
requests_per_minute: int = 20,
|
| 33 |
+
requests_per_hour: int = 200,
|
| 34 |
+
requests_per_day: int = 2000,
|
| 35 |
+
burst_cooldown: float = 30.0,
|
| 36 |
):
|
| 37 |
"""
|
| 38 |
+
Initialize rate limiter with human-like patterns.
|
| 39 |
|
| 40 |
Args:
|
| 41 |
+
max_concurrent: Max simultaneous requests (keep low: 1-5 for safety)
|
| 42 |
+
base_timeout: Base timeout in seconds
|
| 43 |
+
max_queue_size: Maximum queued requests
|
| 44 |
+
requests_per_minute: Soft limit per minute (with jitter)
|
| 45 |
+
requests_per_hour: Soft limit per hour
|
| 46 |
+
requests_per_day: Hard limit per day (account safety)
|
| 47 |
+
burst_cooldown: Cooldown seconds after burst usage
|
| 48 |
"""
|
| 49 |
self.max_concurrent = max_concurrent
|
| 50 |
self.base_timeout = base_timeout
|
| 51 |
self.max_queue_size = max_queue_size
|
| 52 |
+
self.requests_per_minute = requests_per_minute
|
| 53 |
+
self.requests_per_hour = requests_per_hour
|
| 54 |
+
self.requests_per_day = requests_per_day
|
| 55 |
+
self.burst_cooldown = burst_cooldown
|
| 56 |
+
|
| 57 |
self._semaphore = asyncio.Semaphore(max_concurrent)
|
| 58 |
self._current_count = 0
|
| 59 |
self._queued_count = 0
|
| 60 |
self._lock = asyncio.Lock()
|
| 61 |
|
| 62 |
+
# Rate tracking (sliding windows)
|
| 63 |
+
self._minute_requests: list[float] = []
|
| 64 |
+
self._hour_requests: list[float] = []
|
| 65 |
+
self._day_requests: list[float] = []
|
| 66 |
+
self._last_burst_time: Optional[float] = None
|
| 67 |
+
|
| 68 |
# Metrics
|
| 69 |
self._total_requests = 0
|
| 70 |
self._rejected_requests = 0
|
| 71 |
self._last_reset = time.time()
|
| 72 |
|
| 73 |
+
def _cleanup_old_records(self, now: float) -> None:
|
| 74 |
+
"""Remove records older than tracking windows."""
|
| 75 |
+
# Keep last 60 seconds for minute window
|
| 76 |
+
self._minute_requests = [t for t in self._minute_requests if now - t < 60]
|
| 77 |
+
# Keep last 3600 seconds for hour window
|
| 78 |
+
self._hour_requests = [t for t in self._hour_requests if now - t < 3600]
|
| 79 |
+
# Keep last 86400 seconds for day window
|
| 80 |
+
self._day_requests = [t for t in self._day_requests if now - t < 86400]
|
| 81 |
+
|
| 82 |
+
def _get_random_delay(self) -> float:
|
| 83 |
+
"""
|
| 84 |
+
Generate human-like random delay (0.5-3 seconds).
|
| 85 |
+
Mimics natural thinking/typing patterns.
|
| 86 |
+
"""
|
| 87 |
+
# Most delays: 0.5-2 seconds (70%)
|
| 88 |
+
# Some delays: 2-5 seconds (25%)
|
| 89 |
+
# Rare delays: 5-10 seconds (5%)
|
| 90 |
+
rand = random.random()
|
| 91 |
+
if rand < 0.70:
|
| 92 |
+
return random.uniform(0.5, 2.0)
|
| 93 |
+
elif rand < 0.95:
|
| 94 |
+
return random.uniform(2.0, 5.0)
|
| 95 |
+
else:
|
| 96 |
+
return random.uniform(5.0, 10.0)
|
| 97 |
+
|
| 98 |
+
def _check_rate_limits(self, now: float) -> Optional[str]:
|
| 99 |
+
"""
|
| 100 |
+
Check if request exceeds rate limits.
|
| 101 |
+
Returns error message if limit exceeded, None otherwise.
|
| 102 |
+
"""
|
| 103 |
+
self._cleanup_old_records(now)
|
| 104 |
+
|
| 105 |
+
# Check for burst cooldown
|
| 106 |
+
if self._last_burst_time and (now - self._last_burst_time) < self.burst_cooldown:
|
| 107 |
+
remaining = self.burst_cooldown - (now - self._last_burst_time)
|
| 108 |
+
return f"Burst cooldown active. Retry after {int(remaining)}s"
|
| 109 |
+
|
| 110 |
+
# Check daily limit (hard limit)
|
| 111 |
+
if len(self._day_requests) >= self.requests_per_day:
|
| 112 |
+
return "Daily limit reached. Try again tomorrow."
|
| 113 |
+
|
| 114 |
+
# Check hourly limit (soft limit with jitter)
|
| 115 |
+
if len(self._hour_requests) >= self.requests_per_hour:
|
| 116 |
+
# Add random jitter (0-5 minutes) to avoid pattern detection
|
| 117 |
+
jitter = random.randint(0, 300)
|
| 118 |
+
return f"Hourly limit reached. Retry after {60 + jitter}s"
|
| 119 |
+
|
| 120 |
+
# Check minute limit (soft limit with jitter)
|
| 121 |
+
if len(self._minute_requests) >= self.requests_per_minute:
|
| 122 |
+
jitter = random.randint(5, 30)
|
| 123 |
+
return f"Too many requests. Retry after {jitter}s"
|
| 124 |
+
|
| 125 |
+
return None
|
| 126 |
+
|
| 127 |
+
def _record_request(self, now: float) -> None:
|
| 128 |
+
"""Record a new request in tracking windows."""
|
| 129 |
+
self._minute_requests.append(now)
|
| 130 |
+
self._hour_requests.append(now)
|
| 131 |
+
self._day_requests.append(now)
|
| 132 |
+
|
| 133 |
+
# Check if we're in burst mode (>80% of minute limit in last minute)
|
| 134 |
+
if len(self._minute_requests) >= int(self.requests_per_minute * 0.8):
|
| 135 |
+
self._last_burst_time = now
|
| 136 |
+
logger.info(f"Rate limiter: burst usage detected, entering cooldown")
|
| 137 |
+
|
| 138 |
async def acquire(self, request: Optional[Request] = None) -> None:
|
| 139 |
"""
|
| 140 |
+
Acquire permission with human-like delays and anti-detection.
|
| 141 |
|
| 142 |
Args:
|
| 143 |
+
request: Optional FastAPI request for context
|
| 144 |
|
| 145 |
Raises:
|
| 146 |
+
HTTPException: 503 when rate limited or queue full
|
| 147 |
"""
|
| 148 |
+
now = time.time()
|
| 149 |
+
|
| 150 |
async with self._lock:
|
| 151 |
self._total_requests += 1
|
| 152 |
|
| 153 |
+
# Check rate limits first
|
| 154 |
+
rate_limit_error = self._check_rate_limits(now)
|
| 155 |
+
if rate_limit_error:
|
| 156 |
+
self._rejected_requests += 1
|
| 157 |
+
retry_after = int(random.uniform(30, 120))
|
| 158 |
+
logger.warning(f"Rate limiter: {rate_limit_error}")
|
| 159 |
+
raise HTTPException(
|
| 160 |
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
| 161 |
+
detail=rate_limit_error,
|
| 162 |
+
headers={"Retry-After": str(retry_after)},
|
| 163 |
+
)
|
| 164 |
+
|
| 165 |
# Fast rejection if queue is full
|
| 166 |
if self._queued_count >= self.max_queue_size:
|
| 167 |
self._rejected_requests += 1
|
| 168 |
+
retry_after = int(random.uniform(10, 30))
|
| 169 |
logger.warning(
|
| 170 |
f"Rate limiter: queue full ({self._queued_count}/{self.max_queue_size}), "
|
| 171 |
+
f"rejecting with jittered retry"
|
| 172 |
)
|
| 173 |
raise HTTPException(
|
| 174 |
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
| 175 |
+
detail="Server is at capacity. Please try again shortly.",
|
| 176 |
+
headers={"Retry-After": str(retry_after)},
|
| 177 |
)
|
| 178 |
|
| 179 |
self._queued_count += 1
|
| 180 |
current_queue_position = self._queued_count
|
| 181 |
|
| 182 |
+
# Add human-like delay before acquiring slot
|
| 183 |
+
human_delay = self._get_random_delay()
|
| 184 |
+
logger.debug(f"Rate limiter: adding human-like delay {human_delay:.1f}s")
|
| 185 |
+
await asyncio.sleep(human_delay)
|
| 186 |
+
|
| 187 |
+
# Calculate adaptive timeout with jitter
|
| 188 |
+
estimated_wait = current_queue_position * random.uniform(1.5, 3.0)
|
| 189 |
adaptive_timeout = min(estimated_wait, self.base_timeout)
|
| 190 |
|
| 191 |
try:
|
| 192 |
logger.debug(
|
| 193 |
+
f"Rate limiter: request queued (position={current_queue_position}), "
|
| 194 |
f"timeout={adaptive_timeout:.1f}s"
|
| 195 |
)
|
| 196 |
|
|
|
|
| 202 |
async with self._lock:
|
| 203 |
self._queued_count -= 1
|
| 204 |
self._current_count += 1
|
| 205 |
+
self._record_request(now)
|
| 206 |
logger.info(
|
| 207 |
f"Rate limiter: acquired slot ({self._current_count}/{self.max_concurrent}), "
|
| 208 |
+
f"queue={self._queued_count}, "
|
| 209 |
+
f"minute={len(self._minute_requests)}, "
|
| 210 |
+
f"hour={len(self._hour_requests)}, "
|
| 211 |
+
f"day={len(self._day_requests)}"
|
| 212 |
)
|
| 213 |
|
| 214 |
except asyncio.TimeoutError:
|
|
|
|
| 216 |
self._queued_count -= 1
|
| 217 |
self._rejected_requests += 1
|
| 218 |
|
| 219 |
+
retry_after = int(random.uniform(30, 90))
|
| 220 |
logger.warning(
|
| 221 |
+
f"Rate limiter: request timed out after {adaptive_timeout:.1f}s, "
|
|
|
|
| 222 |
f"suggesting retry after {retry_after}s"
|
| 223 |
)
|
| 224 |
|
| 225 |
raise HTTPException(
|
| 226 |
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
| 227 |
+
detail="Request timed out. Please try again.",
|
| 228 |
headers={"Retry-After": str(retry_after)},
|
| 229 |
)
|
| 230 |
|
|
|
|
| 238 |
logger.debug(f"Rate limiter: released slot ({current_count}/{self.max_concurrent})")
|
| 239 |
|
| 240 |
def get_metrics(self) -> dict:
|
| 241 |
+
"""Get current rate limiter metrics including anti-detection stats."""
|
| 242 |
+
now = time.time()
|
| 243 |
+
self._cleanup_old_records(now)
|
| 244 |
+
|
| 245 |
return {
|
| 246 |
+
# Current state
|
| 247 |
"current_requests": self._current_count,
|
| 248 |
"queued_requests": self._queued_count,
|
| 249 |
+
|
| 250 |
+
# Configuration
|
| 251 |
"max_concurrent": self.max_concurrent,
|
| 252 |
"max_queue_size": self.max_queue_size,
|
| 253 |
+
"limits": {
|
| 254 |
+
"per_minute": self.requests_per_minute,
|
| 255 |
+
"per_hour": self.requests_per_hour,
|
| 256 |
+
"per_day": self.requests_per_day,
|
| 257 |
+
},
|
| 258 |
+
|
| 259 |
+
# Rate tracking (current usage)
|
| 260 |
+
"usage": {
|
| 261 |
+
"last_minute": len(self._minute_requests),
|
| 262 |
+
"last_hour": len(self._hour_requests),
|
| 263 |
+
"last_day": len(self._day_requests),
|
| 264 |
+
},
|
| 265 |
+
|
| 266 |
+
# Cooldown status
|
| 267 |
+
"burst_cooldown": {
|
| 268 |
+
"active": self._last_burst_time is not None and (now - self._last_burst_time) < self.burst_cooldown,
|
| 269 |
+
"remaining_seconds": max(0, self.burst_cooldown - (now - self._last_burst_time)) if self._last_burst_time else 0,
|
| 270 |
+
},
|
| 271 |
+
|
| 272 |
+
# Overall metrics
|
| 273 |
"total_requests": self._total_requests,
|
| 274 |
"rejected_requests": self._rejected_requests,
|
| 275 |
"rejection_rate": (
|
|
|
|
| 277 |
if self._total_requests > 0
|
| 278 |
else 0.0
|
| 279 |
),
|
| 280 |
+
"uptime_seconds": now - self._last_reset,
|
| 281 |
}
|
| 282 |
|
| 283 |
def reset_metrics(self) -> None:
|
|
|
|
| 292 |
|
| 293 |
|
| 294 |
def get_rate_limiter() -> RateLimiter:
|
| 295 |
+
"""Get or create the global rate limiter with anti-detection defaults."""
|
| 296 |
global _rate_limiter
|
| 297 |
if _rate_limiter is None:
|
| 298 |
# Configure based on environment or use defaults
|
| 299 |
+
# IMPORTANT: Keep concurrent low to avoid detection!
|
| 300 |
import os
|
| 301 |
+
|
| 302 |
+
max_concurrent = int(os.getenv("GEMINI_MAX_CONCURRENT_REQUESTS", "3"))
|
| 303 |
+
base_timeout = float(os.getenv("GEMINI_QUEUE_TIMEOUT", "60.0"))
|
| 304 |
+
max_queue_size = int(os.getenv("GEMINI_MAX_QUEUE_SIZE", "50"))
|
| 305 |
+
|
| 306 |
+
# Anti-detection rate limits (conservative defaults)
|
| 307 |
+
requests_per_minute = int(os.getenv("GEMINI_REQUESTS_PER_MINUTE", "20"))
|
| 308 |
+
requests_per_hour = int(os.getenv("GEMINI_REQUESTS_PER_HOUR", "200"))
|
| 309 |
+
requests_per_day = int(os.getenv("GEMINI_REQUESTS_PER_DAY", "2000"))
|
| 310 |
+
burst_cooldown = float(os.getenv("GEMINI_BURST_COOLDOWN", "30.0"))
|
| 311 |
|
| 312 |
logger.info(
|
| 313 |
+
f"Rate limiter initialized with ANTI-DETECTION settings:\n"
|
| 314 |
+
f" max_concurrent={max_concurrent} (keep low!)\n"
|
| 315 |
+
f" requests/minute={requests_per_minute}\n"
|
| 316 |
+
f" requests/hour={requests_per_hour}\n"
|
| 317 |
+
f" requests/day={requests_per_day}\n"
|
| 318 |
+
f" burst_cooldown={burst_cooldown}s"
|
| 319 |
)
|
| 320 |
|
| 321 |
_rate_limiter = RateLimiter(
|
| 322 |
max_concurrent=max_concurrent,
|
| 323 |
base_timeout=base_timeout,
|
| 324 |
max_queue_size=max_queue_size,
|
| 325 |
+
requests_per_minute=requests_per_minute,
|
| 326 |
+
requests_per_hour=requests_per_hour,
|
| 327 |
+
requests_per_day=requests_per_day,
|
| 328 |
+
burst_cooldown=burst_cooldown,
|
| 329 |
)
|
| 330 |
return _rate_limiter
|
| 331 |
|