bahi-bh commited on
Commit
5deaeae
·
verified ·
1 Parent(s): d7a3ce1

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +491 -0
app.py ADDED
@@ -0,0 +1,491 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import time
4
+ import logging
5
+ import asyncio
6
+ import threading
7
+ from typing import Any, Dict, List, Optional
8
+ from collections import OrderedDict
9
+ from fastapi import FastAPI, HTTPException, Request
10
+ from fastapi.responses import StreamingResponse, JSONResponse
11
+ from pydantic import BaseModel
12
+ import g4f
13
+
14
+ # =====================================================
15
+ # LOGGING
16
+ # =====================================================
17
+ logging.basicConfig(
18
+ level=logging.INFO,
19
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
20
+ )
21
+ logger = logging.getLogger("g4f-smart-router")
22
+
23
+ # =====================================================
24
+ # COOKIES
25
+ # =====================================================
26
+ def _load_cookies_raw() -> Dict[str, Any]:
27
+ raw_env = (os.getenv("COOKIES_JSON") or "").strip()
28
+ if raw_env:
29
+ try:
30
+ return json.loads(raw_env)
31
+ except Exception as e:
32
+ logger.warning(f"Failed to load cookies from env: {e}")
33
+ try:
34
+ if os.path.exists("cookies.json"):
35
+ with open("cookies.json", "r", encoding="utf-8") as f:
36
+ return json.load(f)
37
+ except Exception as e:
38
+ logger.warning(f"Failed to load cookies from file: {e}")
39
+ return {}
40
+
41
+ def load_cookies() -> str:
42
+ data = _load_cookies_raw()
43
+ if not data:
44
+ return "⚠️ No Cookies"
45
+ try:
46
+ from g4f.cookies import set_cookies
47
+ except Exception:
48
+ return "⚠️ Cookies Found"
49
+ for domain, vals in data.items():
50
+ try:
51
+ dom = domain if "." in domain else f".{domain}.com"
52
+ if isinstance(vals, list):
53
+ vals = {x["name"]: x["value"] for x in vals if isinstance(x, dict)}
54
+ if isinstance(vals, dict):
55
+ set_cookies(dom, vals)
56
+ except Exception as e:
57
+ logger.warning(f"Cookie error for {domain}: {e}")
58
+ return "✅ Cookies Loaded"
59
+
60
+ COOKIE_STATUS = load_cookies()
61
+
62
+ # =====================================================
63
+ # CACHE
64
+ # =====================================================
65
+ class TTLCache:
66
+ def __init__(self, max_size: int = 100, ttl_seconds: int = 300):
67
+ self.cache: OrderedDict = OrderedDict()
68
+ self.max_size = max_size
69
+ self.ttl = ttl_seconds
70
+ self._lock = threading.Lock()
71
+ self._last_cleanup = time.time()
72
+ self._cleanup_interval = 60
73
+
74
+ def _clean_expired(self):
75
+ now = time.time()
76
+ if now - self._last_cleanup < self._cleanup_interval:
77
+ return
78
+ self._last_cleanup = now
79
+ expired = [k for k, (_, ts) in self.cache.items() if now - ts > self.ttl]
80
+ for k in expired:
81
+ del self.cache[k]
82
+
83
+ def get(self, key: str) -> Optional[str]:
84
+ with self._lock:
85
+ if key in self.cache:
86
+ value, _ = self.cache[key]
87
+ self.cache.move_to_end(key)
88
+ return value
89
+ return None
90
+
91
+ def set(self, key: str, value: str):
92
+ with self._lock:
93
+ self._clean_expired()
94
+ if len(self.cache) >= self.max_size:
95
+ self.cache.popitem(last=False)
96
+ self.cache[key] = (value, time.time())
97
+
98
+ CACHE = TTLCache(max_size=100, ttl_seconds=300)
99
+
100
+ # =====================================================
101
+ # PROVIDERS
102
+ # =====================================================
103
+ def get_provider(name: str):
104
+ try:
105
+ return getattr(g4f.Provider, name)
106
+ except:
107
+ return None
108
+
109
+ REAL_PROVIDERS = {
110
+ "Perplexity": get_provider("Perplexity") or get_provider("PerplexityAi"),
111
+ "Copilot": get_provider("Copilot"),
112
+ "Qwen": get_provider("Qwen"),
113
+ "Blackbox": get_provider("Blackbox"),
114
+ "DeepSeek": get_provider("DeepSeek"),
115
+ "Bing": get_provider("Bing"),
116
+ "You": get_provider("You"),
117
+ }
118
+ REAL_PROVIDERS = {k: v for k, v in REAL_PROVIDERS.items() if v}
119
+
120
+ # =====================================================
121
+ # MODEL FALLBACK
122
+ # =====================================================
123
+ PROVIDER_MODELS_FALLBACK = {
124
+ "Perplexity": ["sonar", "sonar-pro", "gpt-4o", "llama-3"],
125
+ "Copilot": ["gpt-4o", "gpt-4", "turbo"],
126
+ "Qwen": ["qwen-max", "qwen-plus", "qwen-turbo", "qwen"],
127
+ "Blackbox": ["gpt-4o", "claude-3", "gemini-pro", "llama-3"],
128
+ "DeepSeek": ["deepseek-chat", "deepseek-coder"],
129
+ "Bing": ["gpt-4o", "gpt-4"],
130
+ "You": ["gpt-4o", "claude", "llama-3"],
131
+ }
132
+
133
+ # =====================================================
134
+ # MODEL DISCOVERY
135
+ # =====================================================
136
+ _PROVIDER_MODEL_CACHE = {}
137
+
138
+ def discover_provider_models(provider_obj: Any, provider_name: str) -> List[str]:
139
+ candidates = []
140
+ for attr in ("models", "model", "default_model", "available_models", "supported_models"):
141
+ try:
142
+ if hasattr(provider_obj, attr):
143
+ val = getattr(provider_obj, attr)
144
+ if isinstance(val, dict):
145
+ candidates.extend(str(k) for k in val.keys())
146
+ elif isinstance(val, (list, tuple, set)):
147
+ candidates.extend(str(i) for i in val)
148
+ elif val:
149
+ candidates.append(str(val))
150
+ except:
151
+ pass
152
+ if not candidates:
153
+ candidates = PROVIDER_MODELS_FALLBACK.get(provider_name, ["gpt-4o"])
154
+ seen = set()
155
+ return [m for m in candidates if not (m in seen or seen.add(m))]
156
+
157
+ # =====================================================
158
+ # STREAM CLEANER
159
+ # =====================================================
160
+ def clean_stream(chunk):
161
+ try:
162
+ if isinstance(chunk, dict):
163
+ if 'choices' in chunk and chunk['choices']:
164
+ delta = chunk['choices'][0].get('delta', {})
165
+ if 'content' in delta:
166
+ return delta['content']
167
+ if 'text' in delta:
168
+ return delta['text']
169
+ return chunk.get('content') or chunk.get('text') or ""
170
+
171
+ if isinstance(chunk, str):
172
+ if chunk and chunk[0] == '{' and chunk[-1] == '}':
173
+ try:
174
+ data = json.loads(chunk)
175
+ if 'choices' in data and data['choices']:
176
+ delta = data['choices'][0].get('delta', {})
177
+ if 'content' in delta:
178
+ return delta['content']
179
+ return data.get('content') or data.get('text') or ""
180
+ except:
181
+ pass
182
+ if '\\' in chunk:
183
+ chunk = chunk.replace('\\n', '\n')
184
+ if '\\r' in chunk:
185
+ chunk = chunk.replace('\\r', '\r')
186
+ if '\\t' in chunk:
187
+ chunk = chunk.replace('\\t', ' ')
188
+ return chunk
189
+ return str(chunk)
190
+ except Exception:
191
+ return ""
192
+
193
+ # =====================================================
194
+ # CHAT LOGIC
195
+ # =====================================================
196
+ def ask(message: str, history, provider_name: str, model_name: str, stop_flag=None):
197
+ message = (message or "").strip()
198
+ if not message:
199
+ yield ""
200
+ return
201
+
202
+ key = f"{provider_name}|{model_name}|{message}"
203
+ cached = CACHE.get(key)
204
+ if cached:
205
+ yield cached
206
+ return
207
+
208
+ msgs = []
209
+ try:
210
+ if history:
211
+ if history and isinstance(history[0], dict):
212
+ for item in history[-40:]:
213
+ if role := item.get("role"):
214
+ if content := item.get("content"):
215
+ msgs.append({"role": str(role), "content": str(content)})
216
+ else:
217
+ for item in history[-20:]:
218
+ if isinstance(item, (list, tuple)) and len(item) == 2:
219
+ if u := item[0]:
220
+ msgs.append({"role": "user", "content": str(u)})
221
+ if a := item[1]:
222
+ msgs.append({"role": "assistant", "content": str(a)})
223
+ except Exception as e:
224
+ logger.warning(f"History error: {e}")
225
+
226
+ msgs.append({"role": "user", "content": message})
227
+
228
+ fallback_providers = [
229
+ provider_name, "Perplexity", "Copilot", "Blackbox", "DeepSeek", "Bing", "You", "Qwen"
230
+ ]
231
+ used = []
232
+
233
+ for pname in fallback_providers:
234
+ if pname in used:
235
+ continue
236
+ used.append(pname)
237
+ pobj = REAL_PROVIDERS.get(pname)
238
+ if not pobj:
239
+ continue
240
+
241
+ if pname not in _PROVIDER_MODEL_CACHE:
242
+ _PROVIDER_MODEL_CACHE[pname] = discover_provider_models(pobj, pname)
243
+
244
+ model_candidates = [model_name] + [x for x in _PROVIDER_MODEL_CACHE[pname] if x != model_name]
245
+
246
+ for m in model_candidates[:12]:
247
+ try:
248
+ stream = g4f.ChatCompletion.create(
249
+ model=m,
250
+ provider=pobj,
251
+ messages=msgs,
252
+ stream=True,
253
+ timeout=30
254
+ )
255
+
256
+ buffer = []
257
+
258
+ for chunk in stream:
259
+ if stop_flag and stop_flag.is_set():
260
+ return
261
+ c = clean_stream(chunk)
262
+ if not c:
263
+ continue
264
+ buffer.append(c)
265
+ yield c
266
+
267
+ full = "".join(buffer)
268
+ if full.strip():
269
+ CACHE.set(key, full)
270
+ return
271
+
272
+ except Exception as e:
273
+ logger.warning(f"Provider {pname} model {m} failed: {e}")
274
+ continue
275
+
276
+ yield "❌ Failed with all providers."
277
+
278
+ # =====================================================
279
+ # FASTAPI
280
+ # =====================================================
281
+ app = FastAPI(title="G4F Smart Router", description="Claude-compatible AI Gateway")
282
+
283
+ API_KEY = os.getenv("API_KEY", "mysecretkey123")
284
+
285
+ class ChatRequest(BaseModel):
286
+ message: str
287
+ provider: str = "Perplexity"
288
+ model: str = "sonar"
289
+ history: List[Any] = []
290
+
291
+ # =====================================================
292
+ # Claude Desktop / Claude Code Compatible Endpoints
293
+ # =====================================================
294
+
295
+ def verify_api_key(request: Request):
296
+ auth = request.headers.get("Authorization", "").strip()
297
+ x_key = request.headers.get("X-API-Key", "").strip()
298
+ x_api_key = request.headers.get("x-api-key", "").strip()
299
+
300
+ if auth.startswith("Bearer "):
301
+ key = auth[7:].strip()
302
+ if key and key == API_KEY:
303
+ return True
304
+ if x_key and x_key == API_KEY:
305
+ return True
306
+ if x_api_key and x_api_key == API_KEY:
307
+ return True
308
+
309
+ raise HTTPException(status_code=401, detail="Invalid API key. Use 'Authorization: Bearer KEY' or 'X-API-Key: KEY'")
310
+
311
+ @app.post("/v1/messages")
312
+ async def v1_messages(request: Request):
313
+ """Anthropic API compatible endpoint for Claude Desktop / Claude Code"""
314
+ verify_api_key(request)
315
+
316
+ body = await request.json()
317
+
318
+ # استخراج الرسالة من تنسيق Anthropic
319
+ messages = body.get("messages", [])
320
+ if not messages:
321
+ raise HTTPException(status_code=400, detail="No messages provided")
322
+
323
+ last_message = messages[-1]
324
+ user_message = last_message.get("content", "")
325
+
326
+ # استخراج النموذج
327
+ model = body.get("model", "sonar")
328
+
329
+ # استخراج النظام (system prompt) إن وجد
330
+ system_prompt = body.get("system", "")
331
+
332
+ # بناء التاريخ من الرسائل السابقة
333
+ history = []
334
+ for msg in messages[:-1]:
335
+ role = msg.get("role", "user")
336
+ content = msg.get("content", "")
337
+ history.append({"role": role, "content": content})
338
+
339
+ # إضافة system prompt إذا موجود
340
+ full_message = user_message
341
+ if system_prompt:
342
+ full_message = f"[System: {system_prompt}]\n\n{user_message}"
343
+
344
+ # الحصول على الرد
345
+ full_response = ""
346
+ for chunk in ask(full_message, history, "Perplexity", model):
347
+ full_response = chunk
348
+
349
+ # إرجاع الرد بتنسيق Anthropic API
350
+ return {
351
+ "id": f"msg_{int(time.time())}_{os.urandom(4).hex()}",
352
+ "type": "message",
353
+ "role": "assistant",
354
+ "content": [{"type": "text", "text": full_response}],
355
+ "model": model,
356
+ "stop_reason": "end_turn",
357
+ "stop_sequence": None,
358
+ "usage": {
359
+ "input_tokens": len(user_message) // 4,
360
+ "output_tokens": len(full_response) // 4
361
+ }
362
+ }
363
+
364
+ @app.post("/v1/messages/stream")
365
+ async def v1_messages_stream(request: Request):
366
+ """Anthropic API compatible streaming endpoint"""
367
+ verify_api_key(request)
368
+
369
+ body = await request.json()
370
+ messages = body.get("messages", [])
371
+ if not messages:
372
+ raise HTTPException(status_code=400, detail="No messages provided")
373
+
374
+ last_message = messages[-1]
375
+ user_message = last_message.get("content", "")
376
+ model = body.get("model", "sonar")
377
+
378
+ async def generate_stream():
379
+ message_id = f"msg_{int(time.time())}_{os.urandom(4).hex()}"
380
+
381
+ # رسالة البدء
382
+ yield f"event: message_start\ndata: {{\"message\": {{\"id\": \"{message_id}\", \"type\": \"message\", \"role\": \"assistant\", \"content\": [], \"model\": \"{model}\", \"stop_reason\": null, \"stop_sequence\": null, \"usage\": {{\"input_tokens\": 0, \"output_tokens\": 0}}}}}}\n\n"
383
+
384
+ # رسالة التدفق
385
+ for chunk in ask(user_message, [], "Perplexity", model):
386
+ yield f"event: content_block_delta\ndata: {{\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {{\"type\": \"text_delta\", \"text\": {json.dumps(chunk, ensure_ascii=False)}}}}}\n\n"
387
+
388
+ # رسالة النهاية
389
+ yield f"event: message_delta\ndata: {{\"type\": \"message_delta\", \"delta\": {{\"stop_reason\": \"end_turn\", \"stop_sequence\": null}}, \"usage\": {{\"output_tokens\": 100}}}}\n\n"
390
+ yield f"event: message_stop\ndata: {{}}\n\n"
391
+
392
+ return StreamingResponse(
393
+ generate_stream(),
394
+ media_type="text/event-stream",
395
+ headers={
396
+ "Cache-Control": "no-cache",
397
+ "Connection": "keep-alive",
398
+ "Content-Type": "text/event-stream"
399
+ }
400
+ )
401
+
402
+ @app.get("/v1/models")
403
+ async def v1_models(request: Request):
404
+ """Anthropic API compatible models endpoint"""
405
+ verify_api_key(request)
406
+
407
+ models = []
408
+ for pname, pobj in REAL_PROVIDERS.items():
409
+ if pname not in _PROVIDER_MODEL_CACHE:
410
+ _PROVIDER_MODEL_CACHE[pname] = discover_provider_models(pobj, pname)
411
+ for model in _PROVIDER_MODEL_CACHE[pname]:
412
+ models.append({
413
+ "id": model,
414
+ "type": "model",
415
+ "display_name": f"{pname} - {model}",
416
+ "created_at": "2024-01-01T00:00:00Z"
417
+ })
418
+
419
+ return {"data": models}
420
+
421
+ # =====================================================
422
+ # ORIGINAL ENDPOINTS (للتوافق مع الواجهة القديمة)
423
+ # =====================================================
424
+
425
+ @app.get("/")
426
+ async def root():
427
+ return {
428
+ "message": "G4F Smart Router is running (Claude-compatible)",
429
+ "endpoints": {
430
+ "GET /": "هذه الصفحة",
431
+ "GET /health": "التحقق من صحة الخادم",
432
+ "GET /v1/models": "قائمة النماذج (Claude-compatible)",
433
+ "POST /v1/messages": "إرسال رسالة (Claude-compatible)",
434
+ "POST /v1/messages/stream": "إرسال رسالة متدفق (Claude-compatible)",
435
+ "GET /providers": "قائمة المزودين (يتطلب مفتاح)",
436
+ "POST /chat": "إرسال رسالة (legacy)",
437
+ "POST /chat/stream": "إرسال رسالة متدفق (legacy)"
438
+ },
439
+ "authentication": "Bearer YOUR_API_KEY or X-API-Key: YOUR_API_KEY",
440
+ "cookies": COOKIE_STATUS,
441
+ "status": "✅ Server is working"
442
+ }
443
+
444
+ @app.get("/health")
445
+ async def health():
446
+ return {"status": "ok", "cookies": COOKIE_STATUS, "providers": list(REAL_PROVIDERS.keys())}
447
+
448
+ @app.post("/chat")
449
+ async def chat(request: Request, chat_req: ChatRequest):
450
+ verify_api_key(request)
451
+
452
+ result = ""
453
+ for chunk in ask(chat_req.message, chat_req.history, chat_req.provider, chat_req.model):
454
+ result = chunk
455
+
456
+ return JSONResponse({"response": result})
457
+
458
+ @app.post("/chat/stream")
459
+ async def chat_stream(request: Request, chat_req: ChatRequest):
460
+ verify_api_key(request)
461
+
462
+ async def generate():
463
+ for chunk in ask(chat_req.message, chat_req.history, chat_req.provider, chat_req.model):
464
+ yield f"data: {json.dumps({'delta': chunk}, ensure_ascii=False)}\n\n"
465
+ yield "data: [DONE]\n\n"
466
+
467
+ return StreamingResponse(generate(), media_type="text/event-stream")
468
+
469
+ @app.get("/providers")
470
+ async def get_providers(request: Request):
471
+ verify_api_key(request)
472
+
473
+ providers_info = {}
474
+ for pname, pobj in REAL_PROVIDERS.items():
475
+ if pname not in _PROVIDER_MODEL_CACHE:
476
+ _PROVIDER_MODEL_CACHE[pname] = discover_provider_models(pobj, pname)
477
+ providers_info[pname] = _PROVIDER_MODEL_CACHE[pname]
478
+
479
+ return JSONResponse({"providers": providers_info})
480
+
481
+ # =====================================================
482
+ # RUN
483
+ # =====================================================
484
+ if __name__ == "__main__":
485
+ import uvicorn
486
+ uvicorn.run(
487
+ "app:app",
488
+ host="0.0.0.0",
489
+ port=int(os.getenv("PORT", 7860)),
490
+ reload=False
491
+ )