bahi-bh commited on
Commit
b8ac474
ยท
verified ยท
1 Parent(s): 1540186

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +410 -196
app.py CHANGED
@@ -9,14 +9,14 @@ import random
9
  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
13
  from typing import Dict, List, Optional, Tuple
14
 
15
  # =========================================================
16
  # 1. ุงู„ุฅุนุฏุงุฏุงุช ุงู„ุนู„ูŠุง (Orchestration Config)
17
  # =========================================================
18
- API_KEY = "sk-your-secret-key"
19
- PORT = 7860
20
  MAX_WORKERS = 50
21
  VALIDATION_INTERVAL = 300
22
  GLOBAL_TIMEOUT = 60
@@ -30,7 +30,10 @@ MODEL_BLACKLIST = [
30
  "audio",
31
  "tts",
32
  "moderation",
33
- "whisper"
 
 
 
34
  ]
35
 
36
  VALIDATION_PROMPT = [
@@ -49,15 +52,25 @@ STATE_LOCK = asyncio.Lock()
49
  REQUEST_LIMITER = asyncio.Semaphore(25)
50
  EXECUTOR = ThreadPoolExecutor(max_workers=MAX_WORKERS)
51
 
52
- app = FastAPI()
53
- app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
 
 
 
 
 
54
 
55
- logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s")
56
  logger = logging.getLogger("ORCHESTRATOR")
57
 
 
58
  def get_stealth_headers():
59
  return {
60
- "User-Agent": f"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{random.randint(120,124)}.0.0.0 Safari/537.36",
 
 
 
 
61
  "Origin": "https://g4f.space",
62
  "Referer": "https://g4f.space/",
63
  "X-Requested-With": "XMLHttpRequest",
@@ -65,45 +78,100 @@ def get_stealth_headers():
65
  "Content-Type": "application/json"
66
  }
67
 
 
68
  # =========================================================
69
- # 2. ู…ุณุชุฎุฑุฌ ุงู„ู…ุญุชูˆู‰ ุงู„ู…ุฑูƒุฒูŠ (Central Extractor)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  # =========================================================
71
  def extract_content(data) -> Optional[str]:
72
  if not isinstance(data, dict):
73
  return None
74
-
75
- if "response" in data: return data["response"]
76
- if "content" in data: return data["content"]
77
- if "text" in data: return data["text"]
78
-
79
- if "choices" in data and isinstance(data["choices"], list) and len(data["choices"]) > 0:
 
 
80
  choice = data["choices"][0]
81
- if "message" in choice and isinstance(choice["message"], dict) and "content" in choice["message"]:
82
- return choice["message"]["content"]
83
- if "text" in choice:
84
- return choice["text"]
85
-
 
 
 
 
 
86
  if "message" in data:
87
- if isinstance(data["message"], dict) and "content" in data["message"]:
88
- return data["message"]["content"]
89
- elif isinstance(data["message"], str):
90
- return data["message"]
91
-
92
- if "data" in data:
93
- if isinstance(data["data"], dict) and "message" in data["data"]:
94
- return data["data"]["message"]
95
-
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  return None
97
 
 
98
  # =========================================================
99
- # 3. ู…ุฒูˆุฏุงุช ุงู„ุฎุฏู…ุฉ (Provider Classes)
100
  # =========================================================
101
  class BaseProvider:
102
  def __init__(self, name: str, url: str):
103
  self.name = name
104
  self.url = url
105
  self.headers = get_stealth_headers()
106
- self.aliases = {}
107
  self.fails = 0
108
  self.success = 0
109
  self.cooldown = 0.0
@@ -115,71 +183,114 @@ class BaseProvider:
115
  if total > 0:
116
  self.health = int((self.success / total) * 100)
117
 
 
 
 
118
  async def fetch_models(self) -> List[str]:
119
- discovered_models = []
120
  loop = asyncio.get_event_loop()
121
  try:
122
  async with REQUEST_LIMITER:
123
- discovered_models = await loop.run_in_executor(EXECUTOR, self._fetch_models_sync)
 
124
  except Exception as e:
125
- logger.debug(f"[{self.name}] Fetch models error: {e}")
126
- return list(set(discovered_models))
127
 
128
  def _fetch_models_sync(self) -> List[str]:
129
- discovered = []
130
- endpoints_to_try = [f"{self.url}/models", self.url, f"{self.url}/v1/models"]
131
-
132
- with requests.Session() as session:
133
- for endpoint in endpoints_to_try:
 
 
 
 
 
 
 
 
134
  try:
135
- resp = session.get(endpoint, headers=self.headers, impersonate="chrome124", timeout=10)
136
- if resp.status_code == 200:
137
- data = resp.json()
138
- # Flat list
139
- if isinstance(data, list) and len(data) > 0 and isinstance(data[0], str):
140
- discovered.extend(data)
141
- # List of dicts
142
- elif isinstance(data, list) and len(data) > 0 and isinstance(data[0], dict) and "id" in data[0]:
143
- discovered.extend([m["id"] for m in data])
144
- # Dictionary structures
145
- elif isinstance(data, dict):
146
- if "data" in data and isinstance(data["data"], list):
147
- discovered.extend([m["id"] for m in data["data"] if "id" in m])
148
- elif "models" in data and isinstance(data["models"], list):
149
- if isinstance(data["models"][0], str):
150
- discovered.extend(data["models"])
151
- elif isinstance(data["models"][0], dict) and "id" in data["models"][0]:
152
- discovered.extend([m["id"] for m in data["models"]])
153
-
154
- if discovered:
155
- break
156
- except:
157
  continue
 
158
  return discovered
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  async def validate_model(self, model: str) -> Tuple[bool, float]:
161
- payload = {
162
- "model": model,
163
- "messages": VALIDATION_PROMPT
164
- }
165
- start_time = time.time()
166
  result = await self.attempt_request(payload)
167
- latency = time.time() - start_time
168
-
169
- if result and result.strip().lower() == "ok":
 
170
  self.success += 1
171
- self.update_health()
172
  self.latency = (self.latency + latency) / 2 if self.latency > 0 else latency
173
- return True, latency
174
  else:
175
  self.fails += 1
176
- self.update_health()
177
- return False, latency
178
 
 
 
 
179
  async def attempt_request(self, payload: dict) -> Optional[str]:
180
- # ุงู„ู…ุทู„ูˆุจ ุงู„ุฃูˆู„: ู†ุณุฎ ุงู„ู€ payload ู‚ุจู„ ุงู„ุชุนุฏูŠู„ ู„ู…ู†ุน ุงู„ู€ Leakage
181
- payload = payload.copy()
182
-
183
  if time.time() < self.cooldown:
184
  return None
185
 
@@ -190,71 +301,117 @@ class BaseProvider:
190
  async with REQUEST_LIMITER:
191
  loop = asyncio.get_event_loop()
192
  content = await loop.run_in_executor(EXECUTOR, self._make_request, payload)
193
-
194
- if content:
195
- self.fails = 0
196
- self.success += 1
197
- self.update_health()
198
- return content
199
-
200
- self.fails += 1
201
  self.update_health()
202
- if self.fails >= 3:
203
- self.cooldown = time.time() + 60
204
- return None
205
- except:
 
 
 
 
 
206
  self.fails += 1
207
  self.update_health()
208
  return None
209
 
210
  def _make_request(self, payload: dict) -> Optional[str]:
211
- with requests.Session() as session:
212
- resp = session.post(self.url, headers=self.headers, json=payload, impersonate="chrome124", timeout=25)
213
- if resp.status_code == 200:
214
- try:
 
 
 
 
 
 
215
  data = resp.json()
216
  content = extract_content(data)
217
- if content is not None:
218
- content_str = str(content)
219
- if len(content_str.strip()) > 0:
220
- return content_str
221
- except:
222
- pass
223
  return None
224
 
 
 
 
 
225
  class GroqProvider(BaseProvider):
226
  def __init__(self):
227
  super().__init__("Groq", "https://g4f.space/api/groq")
228
  self.aliases = {"gpt-4o": "llama-3-70b"}
229
 
 
230
  class GeminiProvider(BaseProvider):
231
  def __init__(self):
232
  super().__init__("Gemini", "https://g4f.space/api/gemini")
233
  self.aliases = {"claude-3-5-sonnet": "gemini-1.5-pro"}
234
 
 
235
  class PollinationsProvider(BaseProvider):
236
  def __init__(self):
237
  super().__init__("Pollinations", "https://g4f.space/api/pollinations")
238
  self.aliases = {"gpt-4o": "gpt-4"}
239
 
 
240
  class OllamaProvider(BaseProvider):
241
  def __init__(self):
242
  super().__init__("Ollama", "https://g4f.space/api/ollama")
243
 
 
244
  class PerplexityProvider(BaseProvider):
245
  def __init__(self):
246
  super().__init__("Perplexity", "https://g4f.space/api/perplexity")
247
 
248
- PROVIDER_INSTANCES = [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  GroqProvider(),
250
  GeminiProvider(),
251
  PollinationsProvider(),
252
  OllamaProvider(),
253
- PerplexityProvider()
 
254
  ]
255
 
 
256
  # =========================================================
257
- # 4. ุฅุฏุงุฑุฉ ุงู„ุญุงู„ุฉ ูˆุงู„ุชุฎุฒูŠู† ุงู„ู…ุคู‚ุช (State & Cache Management)
258
  # =========================================================
259
  async def load_cache():
260
  global WORKING_MODELS, PROVIDER_MODEL_MAP
@@ -262,175 +419,232 @@ async def load_cache():
262
  if os.path.exists(CACHE_FILE):
263
  with open(CACHE_FILE, "r", encoding="utf-8") as f:
264
  data = json.load(f)
265
- async with STATE_LOCK:
266
- WORKING_MODELS = data.get("WORKING_MODELS", {})
267
- PROVIDER_MODEL_MAP = data.get("PROVIDER_MODEL_MAP", {})
268
- logger.info("โœ… Cache loaded successfully.")
269
  except Exception as e:
270
  logger.error(f"โš ๏ธ Cache load error: {e}")
271
 
 
272
  async def save_cache():
273
  try:
274
  async with STATE_LOCK:
275
- data = {
276
- "WORKING_MODELS": WORKING_MODELS,
277
- "PROVIDER_MODEL_MAP": PROVIDER_MODEL_MAP
278
  }
279
  with open(CACHE_FILE, "w", encoding="utf-8") as f:
280
- json.dump(data, f, indent=4, ensure_ascii=False)
 
281
  except Exception as e:
282
  logger.error(f"โš ๏ธ Cache save error: {e}")
283
 
 
284
  # =========================================================
285
- # 5. ู…ุญุฑูƒ ุงู„ุงุณุชูƒุดุงู ูˆุงู„ุชุญู‚ู‚ (Discovery & Validation Engine)
286
  # =========================================================
287
  async def discovery_engine():
288
  await load_cache()
 
289
  while True:
290
- logger.info("๐Ÿ“ก Running Validated Discovery Cycle with Auto-Import...")
291
-
292
- fresh_provider_map = {}
293
- fresh_working_models = {}
294
-
295
- for provider in PROVIDER_INSTANCES:
296
- # 1. Discover
297
- discovered_models = await provider.fetch_models()
298
- clean_models = []
299
-
300
- # 2. Validate ALL imported models
301
- for model in discovered_models:
302
- if any(x in model.lower() for x in MODEL_BLACKLIST):
303
- continue
304
-
305
- is_valid, latency = await provider.validate_model(model)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  if is_valid:
307
  clean_models.append(model)
308
-
309
- # 3. Score & Capability Registry Build
310
  if model not in fresh_working_models:
311
  fresh_working_models[model] = {
312
  "providers": [],
313
  "latency": latency,
314
  "health": provider.health,
315
- "aliases": {}
316
  }
317
-
318
- fresh_working_models[model]["providers"].append(provider.url)
319
- curr_lat = fresh_working_models[model]["latency"]
320
- fresh_working_models[model]["latency"] = (curr_lat + latency) / 2
321
- fresh_working_models[model]["health"] = (fresh_working_models[model]["health"] + provider.health) // 2
322
-
 
323
  fresh_provider_map[provider.url] = clean_models
324
- logger.info(f"๐Ÿ”„ Provider {provider.name}: {len(clean_models)} clean models validated.")
325
-
326
- # 4. Atomic Publish
327
  async with STATE_LOCK:
328
  global PROVIDER_MODEL_MAP, WORKING_MODELS
329
  PROVIDER_MODEL_MAP = fresh_provider_map
330
  WORKING_MODELS = fresh_working_models
331
-
332
- # 5. Cache
333
  await save_cache()
334
- logger.info(f"โœ… Orchestrator Ready. Active Models Count: {len(WORKING_MODELS.keys())}")
335
  await asyncio.sleep(VALIDATION_INTERVAL)
336
 
 
337
  # =========================================================
338
- # 6. ุงู„ู…ุนุงู„ุฌ ุงู„ู…ุฑูƒุฒูŠ (The Orchestration Handler)
339
  # =========================================================
340
  @app.on_event("startup")
341
- async def startup():
342
  asyncio.create_task(discovery_engine())
343
 
344
- @app.api_route("/{path:path}", methods=["GET", "HEAD", "POST"])
 
345
  async def omega_handler(request: Request, path: str):
346
- if request.method in ["GET", "HEAD"] and any(x in path for x in ["", "v1", "models"]):
347
- if "models" in path:
 
 
 
 
 
 
 
 
 
 
 
348
  async with STATE_LOCK:
349
- display = list(WORKING_MODELS.keys()) if WORKING_MODELS else ["gpt-4o", "claude-3-5-sonnet"]
350
- return {"object": "list", "data": [{"id": m, "object": "model", "created": int(time.time()), "owned_by": "omega"} for m in display]}
 
 
 
 
 
 
 
 
 
 
 
351
  return Response(status_code=200)
352
 
353
- if request.method == "POST" and any(x in path for x in ["messages", "completions"]):
354
- # ุงู„ู…ุทู„ูˆุจ ุงู„ุซุงู†ูŠ: ุฅุตู„ุงุญ ู†ุธุงู… ุงู„ุชุญู‚ู‚ ุงู„ุฃู…ู†ูŠ
355
- auth_header = request.headers.get("Authorization", "")
356
- x_api_key = request.headers.get("x-api-key", "")
357
-
358
- token = (
359
- auth_header.replace("Bearer ", "").strip()
360
- if "Bearer " in auth_header
361
- else x_api_key
362
- )
363
 
364
- if API_KEY and token != API_KEY:
365
- raise HTTPException(status_code=401)
 
366
 
 
367
  try:
368
  body = await request.json()
369
- except:
370
- raise HTTPException(status_code=400, detail="Invalid JSON body")
371
-
372
- model = body.get("model", "gpt-4o")
373
  messages = body.get("messages", [])
374
 
375
- # Smart Routing based on Capability Registry
 
 
 
376
  async with STATE_LOCK:
377
  model_info = WORKING_MODELS.get(model)
378
  if model_info and model_info.get("providers"):
379
- target_urls = model_info["providers"].copy()
380
  else:
381
  target_urls = [p.url for p in PROVIDER_INSTANCES]
382
 
383
  providers = [p for p in PROVIDER_INSTANCES if p.url in target_urls]
384
-
385
- # ุงู„ุชุฑุชูŠุจ ุงู„ุฐูƒูŠ ุจู†ุงุกู‹ ุนู„ู‰ ุงู„ูุดู„ุŒ ุงู„ุตุญุฉุŒ ูˆุฒู…ู† ุงู„ุงุณุชุฌุงุจุฉ
386
  providers.sort(key=lambda p: (p.fails, -p.health, p.latency))
387
 
388
- reply = None
 
389
  for provider in providers:
390
  if time.time() < provider.cooldown:
391
  continue
392
  reply = await provider.attempt_request(body)
393
- if reply:
 
394
  break
395
 
396
- # Fallback ุงู„ุฏุงุฎู„ูŠ ุงู„ุขู…ู†
397
  if not reply:
398
  try:
399
  from g4f.client import Client
400
  loop = asyncio.get_event_loop()
 
401
  def fallback_req():
402
- return Client().chat.completions.create(model=model, messages=messages).choices[0].message.content
 
 
 
 
 
403
  reply = await loop.run_in_executor(EXECUTOR, fallback_req)
404
- except:
405
- pass
 
406
 
407
- if not reply:
408
- raise HTTPException(status_code=502, detail="Orchestration Failed: All routes exhausted.")
409
 
410
- # ุงู„ุชูˆุงูู‚ูŠุฉ ุงู„ูƒุงู…ู„ุฉ ู„ู„ุฑุฏูˆุฏ
411
- if "messages" in path:
 
412
  return {
413
- "id": f"msg_{uuid.uuid4()}",
414
- "type": "message",
415
- "role": "assistant",
416
- "model": model,
417
- "content": [{"type": "text", "text": reply}],
418
- "stop_reason": "end_turn"
 
419
  }
420
-
 
421
  return {
422
- "id": f"chatcmpl-{uuid.uuid4()}",
423
- "object": "chat.completion",
424
- "created": int(time.time()),
425
- "model": model,
426
- "choices": [{
427
- "index": 0,
428
- "message": {"role": "assistant", "content": reply},
429
- "finish_reason": "stop"
430
- }]
 
 
 
431
  }
432
 
433
  return Response(status_code=404)
434
 
 
 
 
 
435
  if __name__ == "__main__":
436
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
9
  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
 
30
  "audio",
31
  "tts",
32
  "moderation",
33
+ "whisper",
34
+ "dall-e",
35
+ "stable-diffusion",
36
+ "midjourney"
37
  ]
38
 
39
  VALIDATION_PROMPT = [
 
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",
 
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
 
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
296
 
 
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
 
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
584
+
585
  for provider in providers:
586
  if time.time() < provider.cooldown:
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")