bahi-bh commited on
Commit
499feed
ยท
verified ยท
1 Parent(s): f0b5452

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +298 -425
app.py CHANGED
@@ -10,286 +10,245 @@ from fastapi import FastAPI, HTTPException, Request, Response
10
  from fastapi.middleware.cors import CORSMiddleware
11
  from concurrent.futures import ThreadPoolExecutor
12
  from curl_cffi import requests as curl_requests
13
- from typing import Dict, List, Optional, Tuple
14
 
15
  # =========================================================
16
- # 1. ุงู„ุฅุนุฏุงุฏุงุช ุงู„ุนู„ูŠุง (Orchestration Config)
17
  # =========================================================
18
- API_KEY = os.environ.get("API_KEY", "sk-your-secret-key")
19
- PORT = int(os.environ.get("PORT", 7860))
20
- MAX_WORKERS = 50
21
- VALIDATION_INTERVAL = 300
22
- GLOBAL_TIMEOUT = 60
23
- CACHE_FILE = "models_cache.json"
24
 
 
25
  MODEL_BLACKLIST = [
26
- "embed",
27
- "embedding",
28
- "vision",
29
- "image",
30
- "audio",
31
- "tts",
32
  "moderation",
33
- "whisper",
34
- "dall-e",
35
- "stable-diffusion",
36
- "midjourney"
37
  ]
38
 
39
- VALIDATION_PROMPT = [
40
- {
41
- "role": "user",
42
- "content": "Reply only with: OK"
43
- }
44
- ]
45
-
46
- # Capability Registry
47
- WORKING_MODELS: Dict[str, dict] = {}
48
- PROVIDER_MODEL_MAP: Dict[str, List[str]] = {}
49
 
50
- # Atomic State & Concurrency Control
51
- STATE_LOCK = asyncio.Lock()
52
- REQUEST_LIMITER = asyncio.Semaphore(25)
53
- EXECUTOR = ThreadPoolExecutor(max_workers=MAX_WORKERS)
54
 
55
- app = FastAPI(title="Omega Orchestrator", version="2.0")
56
  app.add_middleware(
57
  CORSMiddleware,
58
  allow_origins=["*"],
59
  allow_methods=["*"],
60
- allow_headers=["*"]
61
  )
62
 
63
- logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
64
- logger = logging.getLogger("ORCHESTRATOR")
65
 
66
 
67
- def get_stealth_headers():
 
 
 
68
  return {
69
  "User-Agent": (
70
  f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
71
  f"AppleWebKit/537.36 (KHTML, like Gecko) "
72
- f"Chrome/{random.randint(120, 124)}.0.0.0 Safari/537.36"
73
  ),
74
- "Origin": "https://g4f.space",
75
- "Referer": "https://g4f.space/",
76
  "X-Requested-With": "XMLHttpRequest",
77
- "Accept": "application/json",
78
- "Content-Type": "application/json"
79
  }
80
 
81
 
82
- # =========================================================
83
- # 2. ุงู„ุชุญู‚ู‚ ู…ู† ุงู„ู…ุตุงุฏู‚ุฉ (Auth Verification) โ€” ู…ูุตู„ุญ
84
- # =========================================================
 
 
85
  def verify_api_key(request: Request) -> bool:
86
  """
87
- ูŠุฏุนู… ุซู„ุงุซ ุทุฑู‚ ู„ู„ู…ุตุงุฏู‚ุฉ:
88
- 1. Authorization: Bearer <key>
89
- 2. x-api-key: <key>
90
- 3. api-key: <key>
91
- ุฅุฐุง ู„ู… ูŠูุถุจุท API_KEY ูุงู„ูˆุตูˆู„ ู…ูุชูˆุญ.
92
  """
93
  if not API_KEY or API_KEY == "sk-your-secret-key":
94
- return True # No key configured โ†’ open access
95
 
96
- auth_header = request.headers.get("Authorization", "")
97
- x_api_key = request.headers.get("x-api-key", "")
98
- api_key_hdr = request.headers.get("api-key", "")
99
 
100
- candidates = []
 
 
101
 
102
- # Bearer token
103
- if auth_header.startswith("Bearer "):
104
- candidates.append(auth_header[len("Bearer "):].strip())
105
-
106
- # x-api-key / api-key headers
107
- if x_api_key:
108
- candidates.append(x_api_key.strip())
109
- if api_key_hdr:
110
- candidates.append(api_key_hdr.strip())
111
 
112
  return any(c == API_KEY for c in candidates)
113
 
114
 
115
  # =========================================================
116
- # 3. ู…ุณุชุฎุฑุฌ ุงู„ู…ุญุชูˆู‰ ุงู„ู…ุฑูƒุฒูŠ (Central Content Extractor)
117
  # =========================================================
118
  def extract_content(data) -> Optional[str]:
119
  if not isinstance(data, dict):
120
  return None
121
 
122
- # Direct content fields
123
- for field in ("response", "content", "text", "output", "result", "generated_text"):
124
- if field in data and isinstance(data[field], str) and data[field].strip():
125
- return data[field]
126
-
127
- # OpenAI-style choices
128
- if "choices" in data and isinstance(data["choices"], list) and data["choices"]:
129
- choice = data["choices"][0]
130
- if isinstance(choice, dict):
131
- msg = choice.get("message", {})
132
- if isinstance(msg, dict) and "content" in msg:
133
- return msg["content"]
134
- if "text" in choice and choice["text"].strip():
135
- return choice["text"]
136
- if "delta" in choice and "content" in choice["delta"]:
137
- return choice["delta"]["content"]
138
-
139
- # Anthropic-style message
140
- if "message" in data:
141
- m = data["message"]
142
- if isinstance(m, dict) and "content" in m:
143
- c = m["content"]
144
- # content can be a list of blocks
145
- if isinstance(c, list):
146
- texts = [b.get("text", "") for b in c if isinstance(b, dict) and b.get("type") == "text"]
147
- combined = "".join(texts)
148
- if combined.strip():
149
- return combined
150
- elif isinstance(c, str) and c.strip():
151
- return c
152
- elif isinstance(m, str) and m.strip():
153
- return m
154
-
155
- # Nested data.message
156
- if "data" in data and isinstance(data["data"], dict):
157
- return extract_content(data["data"])
158
-
159
- # Completion field (older APIs)
160
- if "completion" in data and isinstance(data["completion"], str):
161
- return data["completion"]
162
 
163
  return None
164
 
165
 
166
  # =========================================================
167
- # 4. ู…ุฒูˆุฏุงุช ุงู„ุฎุฏู…ุฉ (Provider Classes)
168
  # =========================================================
169
  class BaseProvider:
170
- def __init__(self, name: str, url: str):
171
- self.name = name
172
- self.url = url
173
- self.headers = get_stealth_headers()
 
174
  self.aliases: Dict[str, str] = {}
175
- self.fails = 0
176
- self.success = 0
177
- self.cooldown = 0.0
178
- self.latency = 0.0
179
- self.health = 100
180
-
181
- def update_health(self):
 
 
 
 
 
 
 
 
 
 
 
182
  total = self.success + self.fails
183
- if total > 0:
184
- self.health = int((self.success / total) * 100)
185
 
186
- # ------------------------------------------------------------------
187
- # Model Discovery
188
- # ------------------------------------------------------------------
189
  async def fetch_models(self) -> List[str]:
190
  loop = asyncio.get_event_loop()
191
  try:
192
  async with REQUEST_LIMITER:
193
- models = await loop.run_in_executor(EXECUTOR, self._fetch_models_sync)
194
- return list(set(models))
195
  except Exception as e:
196
- logger.debug(f"[{self.name}] fetch_models error: {e}")
197
  return []
198
 
199
  def _fetch_models_sync(self) -> List[str]:
200
- """
201
- ูŠุฌุฑุจ ู†ู‚ุงุท ู†ู‡ุงูŠุฉ ู…ุชุนุฏุฏุฉ ู„ุงุณุชุฎุฑุงุฌ ุฌู…ูŠุน ุงู„ู†ู…ุงุฐุฌ ุงู„ู…ุชุงุญุฉ.
202
- ูŠุฏุนู… ูƒุงูุฉ ุงู„ู‡ูŠุงูƒู„ ุงู„ุดุงุฆุนุฉ: ู‚ูˆุงุฆู… ู…ุณุทู‘ุญุฉุŒ ู‚ูˆุงู…ูŠุณุŒ ุจูŠุงู†ุงุช ู…ุชุฏุงุฎู„ุฉ.
203
- """
204
- discovered: List[str] = []
205
- endpoints = [
206
- f"{self.url}/v1/models",
207
- f"{self.url}/models",
208
- self.url,
209
- ]
210
 
211
  with curl_requests.Session() as session:
212
- for endpoint in endpoints:
213
  try:
214
- resp = session.get(
215
- endpoint,
216
- headers=self.headers,
217
- impersonate="chrome124",
218
- timeout=10
219
- )
220
- if resp.status_code != 200:
221
  continue
222
- data = resp.json()
223
- extracted = self._parse_models_response(data)
224
- if extracted:
225
- discovered.extend(extracted)
226
- break # Found models, no need to try other endpoints
227
  except Exception:
228
  continue
229
-
230
- return discovered
231
 
232
  @staticmethod
233
- def _parse_models_response(data) -> List[str]:
234
- """
235
- ูŠุณุชุฎุฑุฌ ู…ุนุฑู‘ูุงุช ุงู„ู†ู…ุงุฐุฌ ู…ู† ุฃูŠ ู‡ูŠูƒู„ ุจูŠุงู†ุงุช ู…ุญุชู…ู„.
236
- """
237
  ids: List[str] = []
238
 
239
  if isinstance(data, list):
240
  for item in data:
241
- if isinstance(item, str):
242
- ids.append(item)
243
  elif isinstance(item, dict):
244
- for key in ("id", "name", "model", "model_id"):
245
- if key in item and isinstance(item[key], str):
246
- ids.append(item[key])
 
247
  break
248
 
249
  elif isinstance(data, dict):
250
- # OpenAI-style: {"data": [...]}
251
- if "data" in data and isinstance(data["data"], list):
252
- ids.extend(BaseProvider._parse_models_response(data["data"]))
253
-
254
- # {"models": [...]}
255
- elif "models" in data and isinstance(data["models"], list):
256
- ids.extend(BaseProvider._parse_models_response(data["models"]))
257
-
258
- # {"result": [...]} or {"results": [...]}
259
- elif "result" in data and isinstance(data["result"], list):
260
- ids.extend(BaseProvider._parse_models_response(data["result"]))
261
- elif "results" in data and isinstance(data["results"], list):
262
- ids.extend(BaseProvider._parse_models_response(data["results"]))
263
-
264
- # Single model dict
265
- elif "id" in data:
266
- ids.append(data["id"])
267
 
268
  return ids
269
 
270
- # ------------------------------------------------------------------
271
- # Validation
272
- # ------------------------------------------------------------------
273
- async def validate_model(self, model: str) -> Tuple[bool, float]:
274
- payload = {"model": model, "messages": VALIDATION_PROMPT}
275
- start = time.time()
276
- result = await self.attempt_request(payload)
277
- latency = time.time() - start
278
-
279
- ok = result is not None and "ok" in result.strip().lower()
280
- if ok:
281
- self.success += 1
282
- self.latency = (self.latency + latency) / 2 if self.latency > 0 else latency
283
- else:
284
- self.fails += 1
285
- self.update_health()
286
- return ok, latency
287
-
288
- # ------------------------------------------------------------------
289
- # Request Handling
290
- # ------------------------------------------------------------------
291
  async def attempt_request(self, payload: dict) -> Optional[str]:
292
- payload = payload.copy() # prevent mutation leakage
293
 
294
  if time.time() < self.cooldown:
295
  return None
@@ -300,284 +259,201 @@ class BaseProvider:
300
  try:
301
  async with REQUEST_LIMITER:
302
  loop = asyncio.get_event_loop()
303
- content = await loop.run_in_executor(EXECUTOR, self._make_request, payload)
304
-
305
- if content:
306
- self.fails = 0
307
- self.success += 1
308
- self.update_health()
309
- return content
310
-
311
- self.fails += 1
312
- self.update_health()
313
- if self.fails >= 3:
314
- self.cooldown = time.time() + 60
315
- return None
316
 
 
 
317
  except Exception:
318
- self.fails += 1
319
- self.update_health()
320
  return None
321
 
322
- def _make_request(self, payload: dict) -> Optional[str]:
323
  with curl_requests.Session() as session:
324
  try:
325
- resp = session.post(
326
- self.url,
327
  headers=self.headers,
328
  json=payload,
329
  impersonate="chrome124",
330
- timeout=25
331
  )
332
- if resp.status_code == 200:
333
- data = resp.json()
334
- content = extract_content(data)
335
- if content and str(content).strip():
336
- return str(content).strip()
337
  except Exception:
338
  pass
339
  return None
340
 
341
 
342
- # ------------------------------------------------------------------
343
- # Provider Definitions
344
- # ------------------------------------------------------------------
345
- class GroqProvider(BaseProvider):
346
- def __init__(self):
347
- super().__init__("Groq", "https://g4f.space/api/groq")
348
- self.aliases = {"gpt-4o": "llama-3-70b"}
349
-
350
-
351
- class GeminiProvider(BaseProvider):
352
- def __init__(self):
353
- super().__init__("Gemini", "https://g4f.space/api/gemini")
354
- self.aliases = {"claude-3-5-sonnet": "gemini-1.5-pro"}
355
-
356
-
357
- class PollinationsProvider(BaseProvider):
358
- def __init__(self):
359
- super().__init__("Pollinations", "https://g4f.space/api/pollinations")
360
- self.aliases = {"gpt-4o": "gpt-4"}
361
-
362
-
363
- class OllamaProvider(BaseProvider):
364
- def __init__(self):
365
- super().__init__("Ollama", "https://g4f.space/api/ollama")
366
-
367
-
368
- class PerplexityProvider(BaseProvider):
369
- def __init__(self):
370
- super().__init__("Perplexity", "https://g4f.space/api/perplexity")
371
-
372
-
373
- class OpenRouterProvider(BaseProvider):
374
- """
375
- ู…ุฒูˆู‘ุฏ ุฅุถุงููŠ: OpenRouter โ€” ูŠุชูŠุญ ุงู„ูˆุตูˆู„ ุฅู„ู‰ ู…ุฆุงุช ุงู„ู†ู…ุงุฐุฌ ุชู„ู‚ุงุฆูŠุงู‹.
376
- """
377
- def __init__(self):
378
- super().__init__("OpenRouter", "https://openrouter.ai/api/v1/chat/completions")
379
- self.models_url = "https://openrouter.ai/api/v1/models"
380
-
381
- async def fetch_models(self) -> List[str]:
382
- loop = asyncio.get_event_loop()
383
- try:
384
- async with REQUEST_LIMITER:
385
- models = await loop.run_in_executor(EXECUTOR, self._fetch_openrouter_models)
386
- return list(set(models))
387
- except Exception as e:
388
- logger.debug(f"[OpenRouter] fetch_models error: {e}")
389
- return []
390
-
391
- def _fetch_openrouter_models(self) -> List[str]:
392
- with curl_requests.Session() as session:
393
- try:
394
- resp = session.get(self.models_url, headers=self.headers, impersonate="chrome124", timeout=10)
395
- if resp.status_code == 200:
396
- data = resp.json()
397
- return self._parse_models_response(data)
398
- except Exception:
399
- pass
400
- return []
401
-
402
-
403
  PROVIDER_INSTANCES: List[BaseProvider] = [
404
- GroqProvider(),
405
- GeminiProvider(),
406
- PollinationsProvider(),
407
- OllamaProvider(),
408
- PerplexityProvider(),
409
- OpenRouterProvider(),
 
 
 
 
410
  ]
411
 
412
 
413
  # =========================================================
414
- # 5. ุฅุฏุงุฑุฉ ุงู„ุญุงู„ุฉ ูˆุงู„ุชุฎุฒูŠู† ุงู„ู…ุคู‚ุช
415
  # =========================================================
416
  async def load_cache():
417
  global WORKING_MODELS, PROVIDER_MODEL_MAP
418
  try:
419
  if os.path.exists(CACHE_FILE):
420
  with open(CACHE_FILE, "r", encoding="utf-8") as f:
421
- data = json.load(f)
422
  async with STATE_LOCK:
423
- WORKING_MODELS = data.get("WORKING_MODELS", {})
424
- PROVIDER_MODEL_MAP = data.get("PROVIDER_MODEL_MAP", {})
425
  logger.info(f"โœ… Cache loaded โ€” {len(WORKING_MODELS)} models.")
426
  except Exception as e:
427
- logger.error(f"โš ๏ธ Cache load error: {e}")
428
 
429
 
430
  async def save_cache():
431
  try:
432
  async with STATE_LOCK:
433
- snapshot = {
434
- "WORKING_MODELS": dict(WORKING_MODELS),
435
- "PROVIDER_MODEL_MAP": dict(PROVIDER_MODEL_MAP)
436
  }
437
  with open(CACHE_FILE, "w", encoding="utf-8") as f:
438
- json.dump(snapshot, f, indent=4, ensure_ascii=False)
439
- logger.info("๐Ÿ’พ Cache saved.")
440
  except Exception as e:
441
- logger.error(f"โš ๏ธ Cache save error: {e}")
442
 
443
 
444
  # =========================================================
445
- # 6. ู…ุญุฑูƒ ุงู„ุงุณุชูƒุดุงู ูˆุงู„ุชุญู‚ู‚
446
  # =========================================================
447
  async def discovery_engine():
 
 
 
 
 
448
  await load_cache()
449
 
450
  while True:
451
- logger.info("๐Ÿ“ก Starting validated discovery cycle โ€ฆ")
452
 
453
- fresh_provider_map: Dict[str, List[str]] = {}
454
- fresh_working_models: Dict[str, dict] = {}
455
 
456
- # Run provider discovery concurrently
457
- tasks = {provider: asyncio.create_task(provider.fetch_models()) for provider in PROVIDER_INSTANCES}
458
 
459
  for provider, task in tasks.items():
460
- discovered = await task
461
- clean_models: List[str] = []
462
-
463
- # Filter & validate
464
- validation_tasks = []
465
- filtered = [
466
- m for m in discovered
467
- if not any(bl in m.lower() for bl in MODEL_BLACKLIST)
468
- ]
469
-
470
- logger.info(f"[{provider.name}] Discovered {len(filtered)} candidate models โ€” validating โ€ฆ")
471
-
472
- for model in filtered:
473
- validation_tasks.append((model, asyncio.create_task(provider.validate_model(model))))
474
-
475
- for model, vtask in validation_tasks:
476
- try:
477
- is_valid, latency = await vtask
478
- except Exception:
479
- is_valid, latency = False, 0.0
480
-
481
- if is_valid:
482
- clean_models.append(model)
483
 
484
- if model not in fresh_working_models:
485
- fresh_working_models[model] = {
486
- "providers": [],
487
- "latency": latency,
488
- "health": provider.health,
489
- "aliases": provider.aliases
490
- }
491
 
492
- info = fresh_working_models[model]
493
- if provider.url not in info["providers"]:
494
- info["providers"].append(provider.url)
495
- info["latency"] = (info["latency"] + latency) / 2
496
- info["health"] = (info["health"] + provider.health) // 2
 
 
 
 
497
 
498
- fresh_provider_map[provider.url] = clean_models
499
- logger.info(f"โœ… [{provider.name}] {len(clean_models)} valid models.")
500
 
501
- # Atomic publish
502
  async with STATE_LOCK:
503
- global PROVIDER_MODEL_MAP, WORKING_MODELS
504
- PROVIDER_MODEL_MAP = fresh_provider_map
505
- WORKING_MODELS = fresh_working_models
506
 
507
  await save_cache()
508
- logger.info(f"๐Ÿš€ Orchestrator ready โ€” {len(WORKING_MODELS)} active models.")
509
- await asyncio.sleep(VALIDATION_INTERVAL)
510
 
511
 
512
  # =========================================================
513
- # 7. ุงู„ู…ุนุงู„ุฌ ุงู„ู…ุฑูƒุฒูŠ (Omega Handler)
514
  # =========================================================
515
  @app.on_event("startup")
516
- async def startup():
517
  asyncio.create_task(discovery_engine())
518
 
519
 
520
  @app.api_route("/{path:path}", methods=["GET", "HEAD", "POST", "OPTIONS"])
521
  async def omega_handler(request: Request, path: str):
522
- # ---- CORS preflight ----
523
- if request.method == "OPTIONS":
524
- return Response(status_code=204, headers={
525
- "Access-Control-Allow-Origin": "*",
526
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
527
- "Access-Control-Allow-Headers": "*"
528
- })
529
 
530
- path_lower = path.lower().strip("/")
531
-
532
- # ---- Model listing ----
533
- if request.method in ("GET", "HEAD") and ("models" in path_lower or path_lower in ("", "v1", "v1/")):
534
- if "models" in path_lower:
535
- async with STATE_LOCK:
536
- model_ids = list(WORKING_MODELS.keys()) if WORKING_MODELS else ["gpt-4o", "claude-3-5-sonnet"]
537
- return {
538
- "object": "list",
539
- "data": [
540
- {
541
- "id": m,
542
- "object": "model",
543
- "created": int(time.time()),
544
- "owned_by": "omega-orchestrator"
545
- }
546
- for m in sorted(model_ids)
547
- ]
548
- }
549
- return Response(status_code=200)
 
 
 
 
 
 
 
550
 
551
- # ---- Chat completions ----
552
- if request.method == "POST" and any(x in path_lower for x in ("messages", "completions", "chat")):
553
 
554
- # --- Auth check (ู…ูุตู„ุญ) ---
555
  if not verify_api_key(request):
556
- raise HTTPException(status_code=401, detail="Unauthorized: invalid or missing API key.")
557
 
558
- # --- Parse body ---
559
  try:
560
  body = await request.json()
561
  except Exception:
562
- raise HTTPException(status_code=400, detail="Invalid JSON body.")
563
 
564
- model = body.get("model", "gpt-4o")
565
  messages = body.get("messages", [])
566
 
567
  if not messages:
568
- raise HTTPException(status_code=400, detail="messages field is required.")
569
 
570
- # --- Smart routing ---
571
  async with STATE_LOCK:
572
- model_info = WORKING_MODELS.get(model)
573
- if model_info and model_info.get("providers"):
574
- target_urls = list(model_info["providers"])
575
- else:
576
- target_urls = [p.url for p in PROVIDER_INSTANCES]
577
 
578
- providers = [p for p in PROVIDER_INSTANCES if p.url in target_urls]
579
-
580
- # Sort: fewest fails โ†’ highest health โ†’ lowest latency
581
  providers.sort(key=lambda p: (p.fails, -p.health, p.latency))
582
 
583
  reply: Optional[str] = None
@@ -587,64 +463,61 @@ async def omega_handler(request: Request, path: str):
587
  continue
588
  reply = await provider.attempt_request(body)
589
  if reply:
590
- logger.info(f"โœ… Served by [{provider.name}] model={model}")
591
  break
592
 
593
- # --- Internal fallback via g4f ---
594
  if not reply:
595
  try:
596
  from g4f.client import Client
597
  loop = asyncio.get_event_loop()
598
-
599
- def fallback_req():
600
- return (
601
- Client()
602
  .chat.completions.create(model=model, messages=messages)
603
- .choices[0].message.content
604
- )
605
-
606
- reply = await loop.run_in_executor(EXECUTOR, fallback_req)
607
- logger.info("๐Ÿ”„ Served via g4f fallback.")
608
  except Exception as e:
609
  logger.warning(f"g4f fallback failed: {e}")
610
 
611
  if not reply:
612
- raise HTTPException(status_code=502, detail="Orchestration Failed: all routes exhausted.")
613
 
614
- # --- Format response ---
615
- # Anthropic messages format
616
- if "messages" in path_lower and "chat" not in path_lower:
617
  return {
618
- "id": f"msg_{uuid.uuid4().hex}",
619
- "type": "message",
620
- "role": "assistant",
621
- "model": model,
622
- "content": [{"type": "text", "text": reply}],
623
  "stop_reason": "end_turn",
624
- "usage": {"input_tokens": 0, "output_tokens": 0}
625
  }
626
 
627
- # OpenAI chat.completions format (default)
628
  return {
629
- "id": f"chatcmpl-{uuid.uuid4().hex}",
630
- "object": "chat.completion",
631
  "created": int(time.time()),
632
- "model": model,
633
  "choices": [
634
  {
635
- "index": 0,
636
- "message": {"role": "assistant", "content": reply},
637
- "finish_reason": "stop"
638
  }
639
  ],
640
- "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
641
  }
642
 
643
  return Response(status_code=404)
644
 
645
 
646
  # =========================================================
647
- # 8. ู†ู‚ุทุฉ ุงู„ุฏุฎูˆู„
648
  # =========================================================
649
  if __name__ == "__main__":
650
  uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info")
 
10
  from fastapi.middleware.cors import CORSMiddleware
11
  from concurrent.futures import ThreadPoolExecutor
12
  from curl_cffi import requests as curl_requests
13
+ from typing import Dict, List, Optional
14
 
15
  # =========================================================
16
+ # 1. ุงู„ุฅุนุฏุงุฏุงุช
17
  # =========================================================
18
+ API_KEY = os.environ.get("API_KEY", "sk-your-secret-key")
19
+ PORT = int(os.environ.get("PORT", 7860))
20
+ MAX_WORKERS = 50
21
+ REFRESH_INTERVAL = 300 # ุซูˆุงู†ูŠ ุจูŠู† ุฏูˆุฑุงุช ุงู„ุงุณุชูƒุดุงู
22
+ CACHE_FILE = "models_cache.json"
 
23
 
24
+ # ู†ู…ุงุฐุฌ ุบูŠุฑ ู†ุตูŠุฉ โ€” ูŠูุณุชุจุนุฏ ุงุณุชูŠุฑุงุฏู‡ุง
25
  MODEL_BLACKLIST = [
26
+ "embed", "embedding",
27
+ "image", "vision",
28
+ "audio", "tts", "whisper",
 
 
 
29
  "moderation",
30
+ "dall-e", "stable-diffusion", "midjourney",
 
 
 
31
  ]
32
 
33
+ # ===================== ุงู„ุญุงู„ุฉ ุงู„ุนุงู…ุฉ =====================
34
+ WORKING_MODELS: Dict[str, dict] = {} # { model_id: {providers, latency, health} }
35
+ PROVIDER_MODEL_MAP: Dict[str, List[str]] = {} # { chat_url: [model_id, ...] }
 
 
 
 
 
 
 
36
 
37
+ STATE_LOCK = asyncio.Lock()
38
+ REQUEST_LIMITER = asyncio.Semaphore(30)
39
+ EXECUTOR = ThreadPoolExecutor(max_workers=MAX_WORKERS)
 
40
 
41
+ app = FastAPI(title="Omega Orchestrator", version="3.0")
42
  app.add_middleware(
43
  CORSMiddleware,
44
  allow_origins=["*"],
45
  allow_methods=["*"],
46
+ allow_headers=["*"],
47
  )
48
 
49
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
50
+ logger = logging.getLogger("OMEGA")
51
 
52
 
53
+ # =========================================================
54
+ # 2. ู…ุณุงุนุฏุงุช
55
+ # =========================================================
56
+ def stealth_headers() -> dict:
57
  return {
58
  "User-Agent": (
59
  f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
60
  f"AppleWebKit/537.36 (KHTML, like Gecko) "
61
+ f"Chrome/{random.randint(120, 126)}.0.0.0 Safari/537.36"
62
  ),
63
+ "Origin": "https://g4f.space",
64
+ "Referer": "https://g4f.space/",
65
  "X-Requested-With": "XMLHttpRequest",
66
+ "Accept": "application/json",
67
+ "Content-Type": "application/json",
68
  }
69
 
70
 
71
+ def is_blacklisted(model_id: str) -> bool:
72
+ low = model_id.lower()
73
+ return any(b in low for b in MODEL_BLACKLIST)
74
+
75
+
76
  def verify_api_key(request: Request) -> bool:
77
  """
78
+ ูŠู‚ุจู„ ุงู„ู…ูุชุงุญ ู…ู† ุซู„ุงุซ ุทุฑู‚:
79
+ - Authorization: Bearer <key>
80
+ - x-api-key: <key>
81
+ - api-key: <key>
82
+ ูˆุตูˆู„ ู…ูุชูˆุญ ุฅุฐุง ู„ู… ูŠูุถุจุท API_KEY.
83
  """
84
  if not API_KEY or API_KEY == "sk-your-secret-key":
85
+ return True
86
 
87
+ candidates: List[str] = []
 
 
88
 
89
+ auth = request.headers.get("Authorization", "")
90
+ if auth.startswith("Bearer "):
91
+ candidates.append(auth[7:].strip())
92
 
93
+ for h in ("x-api-key", "api-key"):
94
+ v = request.headers.get(h, "").strip()
95
+ if v:
96
+ candidates.append(v)
 
 
 
 
 
97
 
98
  return any(c == API_KEY for c in candidates)
99
 
100
 
101
  # =========================================================
102
+ # 3. ุงุณุชุฎุฑุงุฌ ุงู„ู…ุญุชูˆู‰ ู…ู† ุฃูŠ ุฑุฏ JSON
103
  # =========================================================
104
  def extract_content(data) -> Optional[str]:
105
  if not isinstance(data, dict):
106
  return None
107
 
108
+ for f in ("response", "content", "text", "output", "result", "generated_text", "completion"):
109
+ v = data.get(f)
110
+ if isinstance(v, str) and v.strip():
111
+ return v.strip()
112
+
113
+ choices = data.get("choices")
114
+ if isinstance(choices, list) and choices:
115
+ c = choices[0]
116
+ if isinstance(c, dict):
117
+ msg = c.get("message", {})
118
+ if isinstance(msg, dict):
119
+ t = msg.get("content")
120
+ if isinstance(t, str) and t.strip():
121
+ return t.strip()
122
+ txt = c.get("text", "")
123
+ if isinstance(txt, str) and txt.strip():
124
+ return txt.strip()
125
+ delta = c.get("delta", {})
126
+ if isinstance(delta, dict):
127
+ t = delta.get("content", "")
128
+ if isinstance(t, str) and t.strip():
129
+ return t.strip()
130
+
131
+ msg = data.get("message")
132
+ if isinstance(msg, dict):
133
+ c = msg.get("content")
134
+ if isinstance(c, list):
135
+ parts = [b.get("text", "") for b in c if isinstance(b, dict) and b.get("type") == "text"]
136
+ joined = "".join(parts).strip()
137
+ if joined:
138
+ return joined
139
+ elif isinstance(c, str) and c.strip():
140
+ return c.strip()
141
+ elif isinstance(msg, str) and msg.strip():
142
+ return msg.strip()
143
+
144
+ inner = data.get("data")
145
+ if isinstance(inner, dict):
146
+ return extract_content(inner)
 
147
 
148
  return None
149
 
150
 
151
  # =========================================================
152
+ # 4. ูƒู„ุงุณ ุงู„ู…ุฒูˆู‘ุฏ ุงู„ุฃุณุงุณูŠ
153
  # =========================================================
154
  class BaseProvider:
155
+ def __init__(self, name: str, url_chat: str, url_models: str = ""):
156
+ self.name = name
157
+ self.url_chat = url_chat
158
+ self.url_models = url_models or url_chat
159
+ self.headers = stealth_headers()
160
  self.aliases: Dict[str, str] = {}
161
+ self.fails = 0
162
+ self.success = 0
163
+ self.cooldown = 0.0
164
+ self.latency = 0.0
165
+ self.health = 100
166
+
167
+ def _record_success(self, latency: float):
168
+ self.success += 1
169
+ self.latency = (self.latency + latency) / 2 if self.latency else latency
170
+ self._calc_health()
171
+
172
+ def _record_fail(self):
173
+ self.fails += 1
174
+ self._calc_health()
175
+ if self.fails >= 3:
176
+ self.cooldown = time.time() + 60
177
+
178
+ def _calc_health(self):
179
  total = self.success + self.fails
180
+ self.health = int(self.success / total * 100) if total else 100
 
181
 
182
+ # โ”€โ”€ ุฌู„ุจ ุงู„ู†ู…ุงุฐุฌ โ€” ุจุฏูˆู† ุชุญู‚ู‚ุŒ ู…ุจุงุดุฑุฉ ู…ู† ุงู„ู…ุฒูˆู‘ุฏ โ”€โ”€โ”€โ”€โ”€โ”€
 
 
183
  async def fetch_models(self) -> List[str]:
184
  loop = asyncio.get_event_loop()
185
  try:
186
  async with REQUEST_LIMITER:
187
+ raw = await loop.run_in_executor(EXECUTOR, self._fetch_models_sync)
188
+ return [m for m in set(raw) if m and not is_blacklisted(m)]
189
  except Exception as e:
190
+ logger.warning(f"[{self.name}] fetch_models error: {e}")
191
  return []
192
 
193
  def _fetch_models_sync(self) -> List[str]:
194
+ # ู†ู‚ุงุท ุงู„ู†ู‡ุงูŠุฉ ุงู„ู…ุญุชู…ู„ุฉ ุจุงู„ุชุฑุชูŠุจ
195
+ base = self.url_models.rstrip("/")
196
+ chat = self.url_chat.rstrip("/")
197
+ endpoints = list(dict.fromkeys([
198
+ base,
199
+ base + "/v1/models",
200
+ base + "/models",
201
+ chat + "/v1/models",
202
+ chat + "/models",
203
+ ]))
204
 
205
  with curl_requests.Session() as session:
206
+ for ep in endpoints:
207
  try:
208
+ r = session.get(ep, headers=self.headers, impersonate="chrome124", timeout=12)
209
+ if r.status_code != 200:
 
 
 
 
 
210
  continue
211
+ ids = self._parse_models(r.json())
212
+ if ids:
213
+ logger.info(f"[{self.name}] {len(ids)} models โ† {ep}")
214
+ return ids
 
215
  except Exception:
216
  continue
217
+ return []
 
218
 
219
  @staticmethod
220
+ def _parse_models(data) -> List[str]:
221
+ """ูŠุณุชุฎุฑุฌ ู…ุนุฑู‘ูุงุช ุงู„ู†ู…ุงุฐุฌ ู…ู† ุฃูŠ ู‡ูŠูƒู„ JSON ู…ู…ูƒู†."""
 
 
222
  ids: List[str] = []
223
 
224
  if isinstance(data, list):
225
  for item in data:
226
+ if isinstance(item, str) and item.strip():
227
+ ids.append(item.strip())
228
  elif isinstance(item, dict):
229
+ for k in ("id", "name", "model", "model_id", "modelId"):
230
+ v = item.get(k)
231
+ if isinstance(v, str) and v.strip():
232
+ ids.append(v.strip())
233
  break
234
 
235
  elif isinstance(data, dict):
236
+ for key in ("data", "models", "result", "results", "items"):
237
+ sub = data.get(key)
238
+ if isinstance(sub, list):
239
+ found = BaseProvider._parse_models(sub)
240
+ if found:
241
+ return found
242
+ for k in ("id", "name", "model"):
243
+ v = data.get(k)
244
+ if isinstance(v, str) and v.strip():
245
+ ids.append(v.strip())
 
 
 
 
 
 
 
246
 
247
  return ids
248
 
249
+ # โ”€โ”€ ุฅุฑุณุงู„ ุงู„ุทู„ุจ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  async def attempt_request(self, payload: dict) -> Optional[str]:
251
+ payload = payload.copy()
252
 
253
  if time.time() < self.cooldown:
254
  return None
 
259
  try:
260
  async with REQUEST_LIMITER:
261
  loop = asyncio.get_event_loop()
262
+ t0 = time.time()
263
+ text = await loop.run_in_executor(EXECUTOR, self._post_sync, payload)
264
+ lat = time.time() - t0
265
+
266
+ if text:
267
+ self._record_success(lat)
268
+ return text
 
 
 
 
 
 
269
 
270
+ self._record_fail()
271
+ return None
272
  except Exception:
273
+ self._record_fail()
 
274
  return None
275
 
276
+ def _post_sync(self, payload: dict) -> Optional[str]:
277
  with curl_requests.Session() as session:
278
  try:
279
+ r = session.post(
280
+ self.url_chat,
281
  headers=self.headers,
282
  json=payload,
283
  impersonate="chrome124",
284
+ timeout=30,
285
  )
286
+ if r.status_code == 200:
287
+ content = extract_content(r.json())
288
+ if content:
289
+ return content
 
290
  except Exception:
291
  pass
292
  return None
293
 
294
 
295
+ # =========================================================
296
+ # 5. ุชุนุฑูŠู ุงู„ู…ุฒูˆู‘ุฏูŠู†
297
+ # ุฃุถู ุฃูŠ ู…ุฒูˆู‘ุฏ ุฌุฏูŠุฏ ู‡ู†ุง ุจู†ูุณ ุงู„ุทุฑูŠู‚ุฉ
298
+ # =========================================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  PROVIDER_INSTANCES: List[BaseProvider] = [
300
+ BaseProvider("G4F-Groq", "https://g4f.space/api/groq"),
301
+ BaseProvider("G4F-Gemini", "https://g4f.space/api/gemini"),
302
+ BaseProvider("G4F-Pollinations","https://g4f.space/api/pollinations"),
303
+ BaseProvider("G4F-Ollama", "https://g4f.space/api/ollama"),
304
+ BaseProvider("G4F-Perplexity", "https://g4f.space/api/perplexity"),
305
+ BaseProvider("G4F-OpenAI", "https://g4f.space/api/openai"),
306
+ BaseProvider("G4F-DeepSeek", "https://g4f.space/api/deepseek"),
307
+ BaseProvider("G4F-Mistral", "https://g4f.space/api/mistral"),
308
+ BaseProvider("G4F-Claude", "https://g4f.space/api/claude"),
309
+ BaseProvider("G4F-Meta", "https://g4f.space/api/meta"),
310
  ]
311
 
312
 
313
  # =========================================================
314
+ # 6. ุงู„ุชุฎุฒูŠู† ุงู„ู…ุคู‚ุช
315
  # =========================================================
316
  async def load_cache():
317
  global WORKING_MODELS, PROVIDER_MODEL_MAP
318
  try:
319
  if os.path.exists(CACHE_FILE):
320
  with open(CACHE_FILE, "r", encoding="utf-8") as f:
321
+ d = json.load(f)
322
  async with STATE_LOCK:
323
+ WORKING_MODELS = d.get("WORKING_MODELS", {})
324
+ PROVIDER_MODEL_MAP = d.get("PROVIDER_MODEL_MAP", {})
325
  logger.info(f"โœ… Cache loaded โ€” {len(WORKING_MODELS)} models.")
326
  except Exception as e:
327
+ logger.error(f"Cache load error: {e}")
328
 
329
 
330
  async def save_cache():
331
  try:
332
  async with STATE_LOCK:
333
+ snap = {
334
+ "WORKING_MODELS": dict(WORKING_MODELS),
335
+ "PROVIDER_MODEL_MAP": dict(PROVIDER_MODEL_MAP),
336
  }
337
  with open(CACHE_FILE, "w", encoding="utf-8") as f:
338
+ json.dump(snap, f, indent=2, ensure_ascii=False)
 
339
  except Exception as e:
340
+ logger.error(f"Cache save error: {e}")
341
 
342
 
343
  # =========================================================
344
+ # 7. ู…ุญุฑูƒ ุงู„ุงุณุชูƒุดุงู โ€” ุงุณุชูŠุฑุงุฏ ู…ุจุงุดุฑ ุจู„ุง ุชุญู‚ู‚ ู…ุณุจู‚
345
  # =========================================================
346
  async def discovery_engine():
347
+ """
348
+ ูŠุฌู„ุจ ุงู„ู†ู…ุงุฐุฌ ู…ุจุงุดุฑุฉู‹ ู…ู† ูƒู„ ู…ุฒูˆู‘ุฏ ููŠ ู†ูุณ ุงู„ูˆู‚ุช
349
+ ูˆูŠุถูŠูู‡ุง ู„ู„ุณุฌู„ ููˆุฑุงู‹ โ€” ุจุฏูˆู† ุฎุทูˆุฉ ุชุญู‚ู‚ ุชูุนูŠู‚ ุงู„ุงุณุชูŠุฑุงุฏ.
350
+ ุงู„ูู„ุชุฑุฉ ุงู„ูˆุญูŠุฏุฉ: ุงุณุชุจุนุงุฏ ุงู„ู‚ุงุฆู…ุฉ ุงู„ุณูˆุฏุงุก (ุตูˆุฑ/ุตูˆุช/ุชุถู…ูŠู†).
351
+ """
352
  await load_cache()
353
 
354
  while True:
355
+ logger.info("๐Ÿ” Discovery cycle โ€ฆ")
356
 
357
+ fresh_models: Dict[str, dict] = {}
358
+ fresh_map: Dict[str, List[str]] = {}
359
 
360
+ # ุฌู„ุจ ู…ุชูˆุงุฒู ู…ู† ุฌู…ูŠุน ุงู„ู…ุฒูˆู‘ุฏูŠู†
361
+ tasks = {p: asyncio.create_task(p.fetch_models()) for p in PROVIDER_INSTANCES}
362
 
363
  for provider, task in tasks.items():
364
+ try:
365
+ models = await task
366
+ except Exception as e:
367
+ logger.warning(f"[{provider.name}] task error: {e}")
368
+ models = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
370
+ fresh_map[provider.url_chat] = models
 
 
 
 
 
 
371
 
372
+ for m in models:
373
+ if m not in fresh_models:
374
+ fresh_models[m] = {
375
+ "providers": [],
376
+ "latency": 0.0,
377
+ "health": provider.health,
378
+ }
379
+ if provider.url_chat not in fresh_models[m]["providers"]:
380
+ fresh_models[m]["providers"].append(provider.url_chat)
381
 
382
+ logger.info(f"[{provider.name}] โ†’ {len(models)} models imported.")
 
383
 
384
+ # ู†ุดุฑ ุฐุฑูŠ
385
  async with STATE_LOCK:
386
+ global WORKING_MODELS, PROVIDER_MODEL_MAP
387
+ WORKING_MODELS = fresh_models
388
+ PROVIDER_MODEL_MAP = fresh_map
389
 
390
  await save_cache()
391
+ logger.info(f"๐Ÿš€ Ready โ€” {len(WORKING_MODELS)} total models.")
392
+ await asyncio.sleep(REFRESH_INTERVAL)
393
 
394
 
395
  # =========================================================
396
+ # 8. ุงู„ู…ุนุงู„ุฌ ุงู„ู…ุฑูƒุฒูŠ
397
  # =========================================================
398
  @app.on_event("startup")
399
+ async def on_startup():
400
  asyncio.create_task(discovery_engine())
401
 
402
 
403
  @app.api_route("/{path:path}", methods=["GET", "HEAD", "POST", "OPTIONS"])
404
  async def omega_handler(request: Request, path: str):
 
 
 
 
 
 
 
405
 
406
+ # CORS preflight
407
+ if request.method == "OPTIONS":
408
+ return Response(
409
+ status_code=204,
410
+ headers={
411
+ "Access-Control-Allow-Origin": "*",
412
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
413
+ "Access-Control-Allow-Headers": "*",
414
+ },
415
+ )
416
+
417
+ p = path.lower().strip("/")
418
+
419
+ # โ”€โ”€ ู‚ุงุฆู…ุฉ ุงู„ู†ู…ุงุฐุฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
420
+ if request.method in ("GET", "HEAD"):
421
+ if "models" in p or p in ("", "v1", "v1/"):
422
+ if "models" in p:
423
+ async with STATE_LOCK:
424
+ ids = sorted(WORKING_MODELS.keys())
425
+ return {
426
+ "object": "list",
427
+ "data": [
428
+ {"id": m, "object": "model", "created": int(time.time()), "owned_by": "omega"}
429
+ for m in ids
430
+ ],
431
+ }
432
+ return Response(status_code=200)
433
 
434
+ # โ”€โ”€ ุฅุฑุณุงู„ ุฑุณุงุฆู„ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
435
+ if request.method == "POST" and any(x in p for x in ("messages", "completions", "chat")):
436
 
 
437
  if not verify_api_key(request):
438
+ raise HTTPException(status_code=401, detail="Unauthorized.")
439
 
 
440
  try:
441
  body = await request.json()
442
  except Exception:
443
+ raise HTTPException(status_code=400, detail="Invalid JSON.")
444
 
445
+ model = body.get("model", "")
446
  messages = body.get("messages", [])
447
 
448
  if not messages:
449
+ raise HTTPException(status_code=400, detail="messages is required.")
450
 
451
+ # ุงุฎุชูŠุงุฑ ุงู„ู…ุฒูˆู‘ุฏูŠู† ุงู„ู‚ุงุฏุฑูŠู† ุนู„ู‰ ุชู‚ุฏูŠู… ู‡ุฐุง ุงู„ู†ู…ูˆุฐุฌ
452
  async with STATE_LOCK:
453
+ info = WORKING_MODELS.get(model)
454
+ target_urls = list(info["providers"]) if info and info["providers"] else [p.url_chat for p in PROVIDER_INSTANCES]
 
 
 
455
 
456
+ providers = [p for p in PROVIDER_INSTANCES if p.url_chat in target_urls]
 
 
457
  providers.sort(key=lambda p: (p.fails, -p.health, p.latency))
458
 
459
  reply: Optional[str] = None
 
463
  continue
464
  reply = await provider.attempt_request(body)
465
  if reply:
466
+ logger.info(f"โœ… [{provider.name}] served model={model}")
467
  break
468
 
469
+ # Fallback ุนุจุฑ g4f
470
  if not reply:
471
  try:
472
  from g4f.client import Client
473
  loop = asyncio.get_event_loop()
474
+ reply = await loop.run_in_executor(
475
+ EXECUTOR,
476
+ lambda: Client()
 
477
  .chat.completions.create(model=model, messages=messages)
478
+ .choices[0].message.content,
479
+ )
480
+ logger.info("๐Ÿ”„ g4f fallback used.")
 
 
481
  except Exception as e:
482
  logger.warning(f"g4f fallback failed: {e}")
483
 
484
  if not reply:
485
+ raise HTTPException(status_code=502, detail="All routes exhausted.")
486
 
487
+ # โ”€ ุชู†ุณูŠู‚ ุงู„ุฑุฏ โ”€
488
+ if "messages" in p and "chat" not in p:
489
+ # Anthropic format
490
  return {
491
+ "id": f"msg_{uuid.uuid4().hex}",
492
+ "type": "message",
493
+ "role": "assistant",
494
+ "model": model,
495
+ "content": [{"type": "text", "text": reply}],
496
  "stop_reason": "end_turn",
497
+ "usage": {"input_tokens": 0, "output_tokens": 0},
498
  }
499
 
500
+ # OpenAI format
501
  return {
502
+ "id": f"chatcmpl-{uuid.uuid4().hex}",
503
+ "object": "chat.completion",
504
  "created": int(time.time()),
505
+ "model": model,
506
  "choices": [
507
  {
508
+ "index": 0,
509
+ "message": {"role": "assistant", "content": reply},
510
+ "finish_reason": "stop",
511
  }
512
  ],
513
+ "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
514
  }
515
 
516
  return Response(status_code=404)
517
 
518
 
519
  # =========================================================
520
+ # 9. ู†ู‚ุทุฉ ุงู„ุฏุฎูˆู„
521
  # =========================================================
522
  if __name__ == "__main__":
523
  uvicorn.run(app, host="0.0.0.0", port=PORT, log_level="info")