superxu520 commited on
Commit
4e293c6
·
1 Parent(s): 6b2240a

feat: 实现反检测速率限制器,模拟人类行为模式避免被 Google 封禁

Browse files
Files changed (2) hide show
  1. Dockerfile +13 -5
  2. 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
- # IDE 场景建议max_concurrent=15-20, queue_size=50-100
49
- ENV GEMINI_MAX_CONCURRENT_REQUESTS="15"
50
- ENV GEMINI_QUEUE_TIMEOUT="30.0"
51
- ENV GEMINI_MAX_QUEUE_SIZE="100"
 
 
 
 
 
 
 
 
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
- Intelligent rate limiter middleware for high-concurrency scenarios.
3
- Optimized for code IDE integration with adaptive queuing and retry-after hints.
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
- Adaptive rate limiter with smart queuing for IDE workloads.
17
 
18
- Features:
19
- - Configurable concurrent limit
20
- - Adaptive timeout based on queue length
21
- - Retry-After header for client guidance
22
- - Metrics tracking for monitoring
 
23
  """
24
 
25
  def __init__(
26
  self,
27
- max_concurrent: int = 10,
28
- base_timeout: float = 30.0,
29
- max_queue_size: int = 100,
 
 
 
 
30
  ):
31
  """
32
- Initialize rate limiter.
33
 
34
  Args:
35
- max_concurrent: Maximum simultaneous requests (IDE: 10-20 recommended)
36
- base_timeout: Base timeout in seconds (actual timeout adapts to queue)
37
- max_queue_size: Maximum queued requests before immediate rejection
 
 
 
 
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 to process a request with adaptive timeout.
55
 
56
  Args:
57
- request: Optional FastAPI request for context-aware limiting
58
 
59
  Raises:
60
- HTTPException: 503 when queue is full or timeout exceeded
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 immediately"
71
  )
72
  raise HTTPException(
73
  status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
74
- detail="Server is at capacity. Please try again in a few seconds.",
75
- headers={"Retry-After": "5"},
76
  )
77
 
78
  self._queued_count += 1
79
  current_queue_position = self._queued_count
80
 
81
- # Calculate adaptive timeout based on queue position
82
- # Each position in queue adds ~2 seconds (estimated avg request time)
83
- estimated_wait = current_queue_position * 2.0
 
 
 
 
84
  adaptive_timeout = min(estimated_wait, self.base_timeout)
85
 
86
  try:
87
  logger.debug(
88
- f"Rate limiter: request queued at position {current_queue_position}, "
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 = min(int(adaptive_timeout * 1.5), 30)
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=f"Server is busy. Please retry after {retry_after} seconds.",
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": time.time() - self._last_reset,
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
- max_concurrent = int(os.getenv("GEMINI_MAX_CONCURRENT_REQUESTS", "10"))
167
- base_timeout = float(os.getenv("GEMINI_QUEUE_TIMEOUT", "30.0"))
168
- max_queue_size = int(os.getenv("GEMINI_MAX_QUEUE_SIZE", "100"))
 
 
 
 
 
 
 
169
 
170
  logger.info(
171
- f"Rate limiter initialized: max_concurrent={max_concurrent}, "
172
- f"base_timeout={base_timeout}s, max_queue_size={max_queue_size}"
 
 
 
 
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