infinityonline commited on
Commit
9baa398
ยท
verified ยท
1 Parent(s): aca7d3d

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +52 -32
main.py CHANGED
@@ -13,8 +13,9 @@ from fastapi.responses import JSONResponse
13
  # Configuration
14
  # ====================================================================
15
  API_SECRET_KEY = os.getenv("API_SECRET_KEY", "change-me-secret")
16
- POOL_SIZE = int(os.getenv("POOL_SIZE", "3"))
17
- MAX_REQUESTS = int(os.getenv("MAX_REQUESTS", "50")) # ุชุฌุฏูŠุฏ context ูƒู„ N ุทู„ุจ
 
18
 
19
  DUCK_MODELS = {
20
  "gpt-5-mini": "GPT-5 mini",
@@ -135,10 +136,10 @@ class BrowserWorker:
135
  self.busy = False
136
 
137
  async def _do_chat(self, model_label: str, prompt: str) -> str:
138
- # โ”€โ”€ ุชุฃุฎูŠุฑ ุนุดูˆุงุฆูŠ ู„ุชุจุฏูˆ ุงู„ุทู„ุจุงุช ุทุจูŠุนูŠุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
139
  await asyncio.sleep(random.uniform(0.5, 2.0))
140
 
141
- # โ”€โ”€ ุชุฌุฏูŠุฏ context ุฏูˆุฑูŠุงู‹ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
142
  self._request_count += 1
143
  self._total_count += 1
144
  if self._request_count >= MAX_REQUESTS:
@@ -148,11 +149,9 @@ class BrowserWorker:
148
  try:
149
  page.set_default_timeout(180000)
150
 
151
- # โ”€โ”€ 1. ูุชุญ ุงู„ุตูุญุฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
152
  await page.goto("https://duckduckgo.com/aichat", wait_until="domcontentloaded")
153
  await asyncio.sleep(3)
154
 
155
- # โ”€โ”€ 2. Popup ุงุญุชูŠุงุทูŠ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
156
  try:
157
  agree = page.locator('button:has-text("Agree and Continue")')
158
  if await agree.count() > 0 and await agree.first.is_visible():
@@ -161,17 +160,11 @@ class BrowserWorker:
161
  except Exception:
162
  pass
163
 
164
- # โ”€โ”€ 3. ุงู†ุชุธุงุฑ textarea โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
165
  await page.wait_for_selector('textarea[name="user-prompt"]')
166
  print(self.tag(f"Input ready (req #{self._total_count}) โœ“"))
167
 
168
- # โ”€โ”€ 4. ุชุบูŠูŠุฑ ุงู„ู†ู…ูˆุฐุฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
169
  await self._select_model(page, model_label)
170
-
171
- # โ”€โ”€ 5. ุฅุฑุณุงู„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
172
  await self._send(page, prompt)
173
-
174
- # โ”€โ”€ 6. ุงุณุชุฎุฑุงุฌ ุงู„ุฑุฏ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
175
  return await self._extract(page)
176
 
177
  except Exception as e:
@@ -325,16 +318,18 @@ class BrowserWorker:
325
  class BrowserPool:
326
 
327
  def __init__(self):
328
- self.loop = asyncio.new_event_loop()
329
  self.workers: list[BrowserWorker] = []
330
- self.ready_event = threading.Event()
331
- self._thread = threading.Thread(target=self._run, daemon=True)
332
  self._queue: asyncio.Queue | None = None
333
  self._total_requests = 0
 
334
 
335
  def start(self):
336
  self._thread.start()
337
- print(f"[POOL] Starting {POOL_SIZE} browsers (rotation every {MAX_REQUESTS} req)...")
 
338
 
339
  def _run(self):
340
  asyncio.set_event_loop(self.loop)
@@ -355,8 +350,23 @@ class BrowserPool:
355
 
356
  async def _process(self, model_label: str, prompt: str) -> str:
357
  self._total_requests += 1
358
- worker: BrowserWorker = await self._queue.get()
359
- print(f"[POOL] Assigned W{worker.id} (total req: {self._total_requests}) โœ“")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  try:
361
  return await worker.chat(model_label, prompt)
362
  finally:
@@ -369,7 +379,7 @@ class BrowserPool:
369
  future = asyncio.run_coroutine_threadsafe(
370
  self._process(model_label, prompt), self.loop
371
  )
372
- return future.result(timeout=240)
373
 
374
 
375
  pool = BrowserPool()
@@ -399,7 +409,9 @@ def _build_prompt(messages: list) -> str:
399
  if not content.strip():
400
  continue
401
  if role == "system":
402
- parts.append(f"=== SYSTEM INSTRUCTIONS ===\n{content}\n=== END INSTRUCTIONS ===")
 
 
403
  elif role == "assistant":
404
  parts.append(f"[Assistant]: {content}")
405
  else:
@@ -506,7 +518,8 @@ async def chat_completions(request: Request):
506
  return _make_completion(start_time, model, text, messages, tools)
507
  except Exception as e:
508
  print(f"[API] ERROR: {e}")
509
- return JSONResponse(status_code=500, content={"error": {"message": str(e)}})
 
510
 
511
 
512
  @app.post("/v1/responses")
@@ -565,7 +578,8 @@ async def responses(request: Request):
565
  }
566
  except Exception as e:
567
  print(f"[API] ERROR: {e}")
568
- return JSONResponse(status_code=500, content={"error": {"message": str(e)}})
 
569
 
570
 
571
  @app.get("/v1/models")
@@ -583,19 +597,25 @@ async def list_models(request: Request):
583
  async def health():
584
  busy = sum(1 for w in pool.workers if w.busy)
585
  stats = [
586
- {"id": w.id, "busy": w.busy, "total_requests": w._total_count,
587
- "requests_until_rotation": MAX_REQUESTS - w._request_count}
 
 
 
 
588
  for w in pool.workers
589
  ]
590
  return {
591
- "status": "running",
592
- "message": "Duck.ai API Pool Server is active!",
593
- "models": ALL_MODELS,
594
- "pool_size": POOL_SIZE,
595
- "workers_busy": busy,
596
- "workers_free": POOL_SIZE - busy,
597
- "total_requests": pool._total_requests,
598
- "workers": stats,
 
 
599
  }
600
 
601
 
 
13
  # Configuration
14
  # ====================================================================
15
  API_SECRET_KEY = os.getenv("API_SECRET_KEY", "change-me-secret")
16
+ POOL_SIZE = int(os.getenv("POOL_SIZE", "5"))
17
+ MAX_REQUESTS = int(os.getenv("MAX_REQUESTS", "30"))
18
+ QUEUE_TIMEOUT = int(os.getenv("QUEUE_TIMEOUT", "300")) # ุซูˆุงู†ูŠ ุงู†ุชุธุงุฑ Queue
19
 
20
  DUCK_MODELS = {
21
  "gpt-5-mini": "GPT-5 mini",
 
136
  self.busy = False
137
 
138
  async def _do_chat(self, model_label: str, prompt: str) -> str:
139
+ # ุชุฃุฎูŠุฑ ุนุดูˆุงุฆูŠ ู„ุชุจุฏูˆ ุงู„ุทู„ุจุงุช ุทุจูŠุนูŠุฉ
140
  await asyncio.sleep(random.uniform(0.5, 2.0))
141
 
142
+ # ุชุฌุฏูŠุฏ context ุฏูˆุฑูŠุงู‹
143
  self._request_count += 1
144
  self._total_count += 1
145
  if self._request_count >= MAX_REQUESTS:
 
149
  try:
150
  page.set_default_timeout(180000)
151
 
 
152
  await page.goto("https://duckduckgo.com/aichat", wait_until="domcontentloaded")
153
  await asyncio.sleep(3)
154
 
 
155
  try:
156
  agree = page.locator('button:has-text("Agree and Continue")')
157
  if await agree.count() > 0 and await agree.first.is_visible():
 
160
  except Exception:
161
  pass
162
 
 
163
  await page.wait_for_selector('textarea[name="user-prompt"]')
164
  print(self.tag(f"Input ready (req #{self._total_count}) โœ“"))
165
 
 
166
  await self._select_model(page, model_label)
 
 
167
  await self._send(page, prompt)
 
 
168
  return await self._extract(page)
169
 
170
  except Exception as e:
 
318
  class BrowserPool:
319
 
320
  def __init__(self):
321
+ self.loop = asyncio.new_event_loop()
322
  self.workers: list[BrowserWorker] = []
323
+ self.ready_event = threading.Event()
324
+ self._thread = threading.Thread(target=self._run, daemon=True)
325
  self._queue: asyncio.Queue | None = None
326
  self._total_requests = 0
327
+ self._rejected = 0 # ุนุฏุฏ ุงู„ุทู„ุจุงุช ุงู„ู…ุฑููˆุถุฉ ุจุณุจุจ timeout
328
 
329
  def start(self):
330
  self._thread.start()
331
+ print(f"[POOL] Starting {POOL_SIZE} browsers "
332
+ f"(rotation every {MAX_REQUESTS} req, queue timeout {QUEUE_TIMEOUT}s)...")
333
 
334
  def _run(self):
335
  asyncio.set_event_loop(self.loop)
 
350
 
351
  async def _process(self, model_label: str, prompt: str) -> str:
352
  self._total_requests += 1
353
+
354
+ # ุงู†ุชุธุฑ worker ุญุฑ โ€” ุจุญุฏ ุฃู‚ุตู‰ QUEUE_TIMEOUT ุซุงู†ูŠุฉ
355
+ try:
356
+ worker: BrowserWorker = await asyncio.wait_for(
357
+ self._queue.get(), timeout=QUEUE_TIMEOUT
358
+ )
359
+ except asyncio.TimeoutError:
360
+ self._rejected += 1
361
+ print(f"[POOL] โš ๏ธ Queue timeout! All {POOL_SIZE} workers busy. "
362
+ f"Rejected: {self._rejected}")
363
+ raise RuntimeError(
364
+ f"All {POOL_SIZE} workers are busy. "
365
+ f"Please retry in a moment. (rejected total: {self._rejected})"
366
+ )
367
+
368
+ print(f"[POOL] Assigned W{worker.id} "
369
+ f"(total req: {self._total_requests}) โœ“")
370
  try:
371
  return await worker.chat(model_label, prompt)
372
  finally:
 
379
  future = asyncio.run_coroutine_threadsafe(
380
  self._process(model_label, prompt), self.loop
381
  )
382
+ return future.result(timeout=QUEUE_TIMEOUT + 240)
383
 
384
 
385
  pool = BrowserPool()
 
409
  if not content.strip():
410
  continue
411
  if role == "system":
412
+ parts.append(
413
+ f"=== SYSTEM INSTRUCTIONS ===\n{content}\n=== END INSTRUCTIONS ==="
414
+ )
415
  elif role == "assistant":
416
  parts.append(f"[Assistant]: {content}")
417
  else:
 
518
  return _make_completion(start_time, model, text, messages, tools)
519
  except Exception as e:
520
  print(f"[API] ERROR: {e}")
521
+ status = 503 if "busy" in str(e).lower() else 500
522
+ return JSONResponse(status_code=status, content={"error": {"message": str(e)}})
523
 
524
 
525
  @app.post("/v1/responses")
 
578
  }
579
  except Exception as e:
580
  print(f"[API] ERROR: {e}")
581
+ status = 503 if "busy" in str(e).lower() else 500
582
+ return JSONResponse(status_code=status, content={"error": {"message": str(e)}})
583
 
584
 
585
  @app.get("/v1/models")
 
597
  async def health():
598
  busy = sum(1 for w in pool.workers if w.busy)
599
  stats = [
600
+ {
601
+ "id": w.id,
602
+ "busy": w.busy,
603
+ "total_requests": w._total_count,
604
+ "requests_until_rotation": MAX_REQUESTS - w._request_count,
605
+ }
606
  for w in pool.workers
607
  ]
608
  return {
609
+ "status": "running",
610
+ "message": "Duck.ai API Pool Server is active!",
611
+ "models": ALL_MODELS,
612
+ "pool_size": POOL_SIZE,
613
+ "workers_busy": busy,
614
+ "workers_free": POOL_SIZE - busy,
615
+ "total_requests": pool._total_requests,
616
+ "rejected_requests": pool._rejected,
617
+ "queue_timeout_sec": QUEUE_TIMEOUT,
618
+ "workers": stats,
619
  }
620
 
621