bahi-bh commited on
Commit
001f551
·
verified ·
1 Parent(s): 152d78a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +538 -118
app.py CHANGED
@@ -1,135 +1,555 @@
1
- from flask import Flask, request, jsonify
2
- from flask_cors import CORS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import g4f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
- app = Flask(__name__)
6
- CORS(app)
7
-
8
- # ============================================
9
- # قائمة الموديلات
10
- # ============================================
11
- MODELS = {
12
- "gpt-4o": "gpt-4o",
13
- "gpt-4o-mini": "gpt-4o-mini",
14
- "gpt-3.5-turbo": "gpt-3.5-turbo",
15
- "gemini-2.0-flash": "gemini-2.0-flash",
16
- }
17
-
18
-
19
- @app.route("/", methods=["GET"])
20
- def home():
21
- """الصفحة الرئيسية - API Info"""
22
- return jsonify({
23
- "status": "running ✅",
24
- "service": "G4F API Server",
25
- "endpoints": {
26
- "/chat": "POST - إرسال رسالة",
27
- "/models": "GET - قائمة الموديلات",
28
- "/health": "GET - فحص الحالة",
 
29
  },
30
- "usage": {
31
- "method": "POST",
32
- "url": "/chat",
33
- "body": {
34
- "message": "مرحباً",
35
- "model": "gpt-4o-mini",
36
- "history": []
37
- }
38
- }
39
- })
40
-
41
-
42
- @app.route("/health", methods=["GET"])
43
- def health():
44
- """فحص حالة السيرفر"""
45
- return jsonify({"status": "healthy ✅"})
46
-
47
-
48
- @app.route("/models", methods=["GET"])
49
- def list_models():
50
- """قائمة الموديلات المتاحة"""
51
- return jsonify({
52
- "models": list(MODELS.keys()),
53
- "default": "gpt-4o-mini"
54
- })
55
-
56
-
57
- @app.route("/chat", methods=["POST"])
58
- def chat():
 
 
 
 
 
 
 
59
  """إرسال رسالة والحصول على رد"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  try:
61
- data = request.get_json()
62
-
63
- # التحقق من البيانات
64
- if not data or "message" not in data:
65
- return jsonify({
66
- "error": "الرجاء إرسال 'message' في الطلب"
67
- }), 400
68
-
69
- message = data["message"]
70
- model = data.get("model", "gpt-4o-mini")
71
- history = data.get("history", [])
72
-
73
- # بناء الرسائل
74
- messages = []
75
- for msg in history:
76
- messages.append(msg)
77
- messages.append({"role": "user", "content": message})
78
-
79
- # إرسال الطلب
80
- response = g4f.ChatCompletion.create(
81
- model=model,
82
  messages=messages,
 
83
  stream=False,
84
  )
85
 
86
- return jsonify({
87
- "success": True,
88
- "response": response,
89
- "model": model,
90
- "history": messages + [{"role": "assistant", "content": response}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  })
92
 
93
  except Exception as e:
94
- return jsonify({
95
- "success": False,
96
- "error": str(e),
97
- "tip": "جرب تغيير الموديل أو أعد المحاولة"
98
- }), 500
99
-
100
-
101
- # ============================================
102
- # سيرفر WebSocket للبث المباشر (Stream)
103
- # ============================================
104
- @app.route("/chat/stream", methods=["POST"])
105
- def chat_stream():
106
- """بث مباشر للرد"""
107
- try:
108
- data = request.get_json()
109
- message = data["message"]
110
- model = data.get("model", "gpt-4o-mini")
111
-
112
- def generate():
113
- response = g4f.ChatCompletion.create(
114
- model=model,
115
- messages=[{"role": "user", "content": message}],
116
- stream=True,
117
- )
118
- for chunk in response:
119
- yield chunk
120
-
121
- return app.response_class(
122
- generate(),
123
- mimetype='text/plain'
124
  )
125
 
126
- except Exception as e:
127
- return jsonify({"error": str(e)}), 500
128
 
 
 
 
129
 
130
  if __name__ == "__main__":
131
- print("=" * 50)
132
- print("🚀 G4F API Server Started!")
133
- print("📡 http://0.0.0.0:8080")
134
- print("=" * 50)
135
- app.run(host="0.0.0.0", port=7860, debug=False)
 
 
 
 
 
1
+ """
2
+ Universal AI Gateway - G4F Backend with FastAPI
3
+ Enhanced version with production-grade features
4
+ """
5
+
6
+ from fastapi import FastAPI, Request, HTTPException
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.responses import StreamingResponse, JSONResponse
9
+ from pydantic import BaseModel, Field
10
+ from typing import List, Optional
11
+ from dataclasses import dataclass
12
+ from enum import Enum
13
+ from contextlib import asynccontextmanager
14
+
15
+ import json
16
+ import time
17
+ import uuid
18
+ import logging
19
+ import os
20
+ import asyncio
21
+
22
  import g4f
23
+ from g4f.client import Client
24
+
25
+
26
+ # =====================================================
27
+ # LOGGING - سجلات مُحسّنة مع ألوان في الـ terminal
28
+ # =====================================================
29
+
30
+ logging.basicConfig(
31
+ level=logging.INFO,
32
+ format="%(asctime)s | %(levelname)-8s | %(message)s",
33
+ datefmt="%Y-%m-%d %H:%M:%S"
34
+ )
35
+ logger = logging.getLogger("ai-gateway")
36
+
37
+
38
+ # =====================================================
39
+ # CONFIG - قراءة من متغيرات البيئة
40
+ # =====================================================
41
+
42
+ API_KEY: str = os.getenv("API_KEY", "")
43
+ REQUEST_TIMEOUT: int = int(os.getenv("REQUEST_TIMEOUT", "120"))
44
+ MAX_RETRIES: int = int(os.getenv("MAX_RETRIES", "2"))
45
+ MAX_MESSAGES: int = int(os.getenv("MAX_MESSAGES", "50"))
46
+ HOST: str = os.getenv("HOST", "0.0.0.0")
47
+ PORT: int = int(os.getenv("PORT", "7860"))
48
+
49
+ # =====================================================
50
+ # FALLBACK MODELS - نماذج بديلة عند الفشل
51
+ # =====================================================
52
+
53
+ FALLBACK_MODELS: List[str] = [
54
+ "gpt-4o-mini",
55
+ "gpt-4o",
56
+ "gemini-2.0-flash",
57
+ "llama-3.1-70b",
58
+ ]
59
+
60
+
61
+ # =====================================================
62
+ # Lifespan - إدارة دورة حياة التطبيق
63
+ # =====================================================
64
+
65
+ @asynccontextmanager
66
+ async def lifespan(app: FastAPI):
67
+ logger.info(f"🚀 AI Gateway starting on {HOST}:{PORT}")
68
+ yield
69
+ logger.info("👋 AI Gateway shutting down")
70
+
71
+
72
+ # =====================================================
73
+ # FASTAPI APP
74
+ # =====================================================
75
+
76
+ app = FastAPI(
77
+ title="Universal AI Gateway",
78
+ version="6.0.0",
79
+ description="OpenAI-compatible API powered by G4F",
80
+ lifespan=lifespan,
81
+ )
82
+
83
+
84
+ # =====================================================
85
+ # CORS - مُحسّن
86
+ # =====================================================
87
+
88
+ allowed_origins = os.getenv("ALLOWED_ORIGINS", "*")
89
+ origins_list = (
90
+ ["*"]
91
+ if allowed_origins == "*"
92
+ else [o.strip() for o in allowed_origins.split(",")]
93
+ )
94
+
95
+ app.add_middleware(
96
+ CORSMiddleware,
97
+ allow_origins=origins_list,
98
+ allow_credentials=True,
99
+ allow_methods=["*"],
100
+ allow_headers=["*"],
101
+ )
102
+
103
+
104
+ # =====================================================
105
+ # RATE LIMITER - مُحدد الطلبات
106
+ # =====================================================
107
+
108
+ @dataclass
109
+ class RateLimiter:
110
+ requests_per_minute: int = 30
111
+ requests: dict = None
112
+
113
+ def __post_init__(self):
114
+ self.requests = {}
115
+
116
+ def check(self, client_ip: str) -> bool:
117
+ now = time.time()
118
+ window_start = now - 60
119
+
120
+ # تنظيف الطلبات القديمة
121
+ if client_ip in self.requests:
122
+ self.requests[client_ip] = [
123
+ t for t in self.requests[client_ip]
124
+ if t > window_start
125
+ ]
126
+ else:
127
+ self.requests[client_ip] = []
128
+
129
+ if len(self.requests[client_ip]) >= self.requests_per_minute:
130
+ return False
131
+
132
+ self.requests[client_ip].append(now)
133
+ return True
134
+
135
+
136
+ rate_limiter = RateLimiter(requests_per_minute=60)
137
+
138
+
139
+ # =====================================================
140
+ # PYDANTIC MODELS
141
+ # =====================================================
142
+
143
+ class Message(BaseModel):
144
+ role: str = Field(
145
+ ...,
146
+ pattern="^(system|user|assistant)$",
147
+ description="Message role"
148
+ )
149
+ content: str = Field(
150
+ ...,
151
+ min_length=1,
152
+ max_length=32000,
153
+ description="Message content"
154
+ )
155
+
156
+
157
+ class ChatRequest(BaseModel):
158
+ model: str = Field(
159
+ ...,
160
+ min_length=1,
161
+ description="Model identifier"
162
+ )
163
+ messages: List[Message] = Field(
164
+ ...,
165
+ min_length=1,
166
+ max_length=MAX_MESSAGES,
167
+ description="Chat messages"
168
+ )
169
+ stream: bool = Field(
170
+ default=False,
171
+ description="Enable streaming"
172
+ )
173
+ temperature: Optional[float] = Field(
174
+ default=0.7,
175
+ ge=0.0,
176
+ le=2.0,
177
+ description="Sampling temperature"
178
+ )
179
+ max_tokens: Optional[int] = Field(
180
+ default=4096,
181
+ ge=1,
182
+ le=128000,
183
+ description="Maximum tokens"
184
+ )
185
+
186
+
187
+ # =====================================================
188
+ # AUTHENTICATION
189
+ # =====================================================
190
+
191
+ def verify_api_key(request: Request) -> bool:
192
+ """التحقق من مفتاح API"""
193
+
194
+ # لا يوجد مفتاح مُعرّف = السماح للجميع
195
+ if not API_KEY:
196
+ return True
197
+
198
+ auth_header = request.headers.get("Authorization")
199
+ if not auth_header:
200
+ raise HTTPException(
201
+ status_code=401,
202
+ detail={
203
+ "error": {
204
+ "message": "Authorization header required",
205
+ "type": "authentication_error",
206
+ }
207
+ },
208
+ )
209
+
210
+ if not auth_header.startswith("Bearer "):
211
+ raise HTTPException(
212
+ status_code=401,
213
+ detail={
214
+ "error": {
215
+ "message": "Invalid authorization format. Use: Bearer <key>",
216
+ "type": "authentication_error",
217
+ }
218
+ },
219
+ )
220
+
221
+ token = auth_header[7:].strip()
222
+ if token != API_KEY:
223
+ raise HTTPException(
224
+ status_code=403,
225
+ detail={
226
+ "error": {
227
+ "message": "Invalid API key",
228
+ "type": "authentication_error",
229
+ }
230
+ },
231
+ )
232
+
233
+ return True
234
+
235
+
236
+ # =====================================================
237
+ # MODEL HELPER - طلب بديل عند الفشل
238
+ # =====================================================
239
+
240
+ def create_chat(
241
+ messages: list,
242
+ model: str,
243
+ stream: bool = False,
244
+ max_retries: int = MAX_RETRIES,
245
+ ) -> any:
246
+ """إرسال طلب مع إعادة المحاولة على نماذج بديلة"""
247
+
248
+ models_to_try = [model] + [
249
+ m for m in FALLBACK_MODELS
250
+ if m != model
251
+ ]
252
+
253
+ last_error = None
254
+
255
+ for model_name in models_to_try[:max_retries + 1]:
256
+
257
+ try:
258
+ client = Client()
259
+ logger.info(f"Trying model: {model_name}")
260
+
261
+ response = client.chat.completions.create(
262
+ model=model_name,
263
+ messages=messages,
264
+ stream=stream,
265
+ )
266
+
267
+ logger.info(f"✅ Success with model: {model_name}")
268
+ return response, model_name
269
+
270
+ except Exception as e:
271
+ last_error = e
272
+ logger.warning(
273
+ f"❌ Model {model_name} failed: {e}"
274
+ )
275
+ continue
276
+
277
+ raise last_error
278
+
279
+
280
+ # =====================================================
281
+ # ROOT
282
+ # =====================================================
283
 
284
+ @app.get("/")
285
+ async def root():
286
+ return {
287
+ "service": "Universal AI Gateway",
288
+ "version": "6.0.0",
289
+ "status": "online",
290
+ }
291
+
292
+
293
+ # =====================================================
294
+ # HEALTH CHECK
295
+ # =====================================================
296
+
297
+ @app.get("/health")
298
+ async def health_check():
299
+ """فحص صحة الخدمة"""
300
+ return {
301
+ "status": "healthy",
302
+ "timestamp": int(time.time()),
303
+ "version": "6.0.0",
304
+ "config": {
305
+ "auth_required": bool(API_KEY),
306
+ "timeout": REQUEST_TIMEOUT,
307
+ "max_retries": MAX_RETRIES,
308
+ "max_messages": MAX_MESSAGES,
309
  },
310
+ }
311
+
312
+
313
+ # =====================================================
314
+ # LOAD AVAILABLE MODELS
315
+ # =====================================================
316
+
317
+ @app.get("/v1/models")
318
+ async def get_models():
319
+ """قائمة النماذج المتاحة"""
320
+
321
+ models_data = []
322
+
323
+ try:
324
+ if hasattr(g4f.models, "_all_models"):
325
+ all_models = list(g4f.models._all_models)
326
+ for model in all_models:
327
+ models_data.append({
328
+ "id": str(model),
329
+ "object": "model",
330
+ "created": int(time.time()),
331
+ "owned_by": "g4f",
332
+ })
333
+
334
+ except Exception as e:
335
+ logger.error(f"Failed to load models: {e}")
336
+
337
+ return {"object": "list", "data": models_data}
338
+
339
+
340
+ # =====================================================
341
+ # CHAT COMPLETIONS - نقطة النهاية الرئيسية
342
+ # =====================================================
343
+
344
+ @app.post("/v1/chat/completions")
345
+ async def chat_completions(request: Request, body: ChatRequest):
346
  """إرسال رسالة والحصول على رد"""
347
+
348
+ # 1. التحقق من المصادقة
349
+ verify_api_key(request)
350
+
351
+ # 2. التحقق من حد الطلبات
352
+ client_ip = request.client.host
353
+ if not rate_limiter.check(client_ip):
354
+ raise HTTPException(
355
+ status_code=429,
356
+ detail={
357
+ "error": {
358
+ "message": "Rate limit exceeded. Try again later.",
359
+ "type": "rate_limit_error",
360
+ }
361
+ },
362
+ )
363
+
364
+ # 3. تحويل الرسائل
365
+ messages = [
366
+ {"role": m.role, "content": m.content}
367
+ for m in body.messages
368
+ ]
369
+
370
+ request_id = f"chatcmpl-{uuid.uuid4().hex[:24]}"
371
+ logger.info(
372
+ f"📨 Request [{request_id}] "
373
+ f"model={body.model} stream={body.stream}"
374
+ )
375
+
376
+ # =====================================
377
+ # STREAMING RESPONSE
378
+ # =====================================
379
+
380
+ if body.stream:
381
+
382
+ def generate_stream():
383
+ try:
384
+ response, used_model = create_chat(
385
+ messages=messages,
386
+ model=body.model,
387
+ stream=True,
388
+ )
389
+
390
+ has_content = False
391
+
392
+ for chunk in response:
393
+ try:
394
+ content = ""
395
+
396
+ if (
397
+ hasattr(chunk, "choices")
398
+ and chunk.choices
399
+ and len(chunk.choices) > 0
400
+ and hasattr(chunk.choices[0], "delta")
401
+ and chunk.choices[0].delta
402
+ and chunk.choices[0].delta.content
403
+ ):
404
+ content = chunk.choices[0].delta.content
405
+
406
+ if not content:
407
+ continue
408
+
409
+ has_content = True
410
+
411
+ payload = {
412
+ "id": request_id,
413
+ "object": "chat.completion.chunk",
414
+ "created": int(time.time()),
415
+ "model": used_model,
416
+ "choices": [
417
+ {
418
+ "index": 0,
419
+ "delta": {"content": content},
420
+ "finish_reason": None,
421
+ }
422
+ ],
423
+ }
424
+
425
+ yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
426
+
427
+ except Exception as chunk_err:
428
+ logger.warning(
429
+ f"Chunk processing error: {chunk_err}"
430
+ )
431
+ continue
432
+
433
+ if not has_content:
434
+ error_payload = {
435
+ "error": {
436
+ "message": "Provider returned empty stream",
437
+ "type": "empty_stream",
438
+ }
439
+ }
440
+ yield f"data: {json.dumps(error_payload)}\n\n"
441
+
442
+ # إشارة النهاية
443
+ final_payload = {
444
+ "id": request_id,
445
+ "object": "chat.completion.chunk",
446
+ "created": int(time.time()),
447
+ "model": body.model,
448
+ "choices": [
449
+ {
450
+ "index": 0,
451
+ "delta": {},
452
+ "finish_reason": "stop",
453
+ }
454
+ ],
455
+ }
456
+ yield f"data: {json.dumps(final_payload)}\n\n"
457
+ yield "data: [DONE]\n\n"
458
+
459
+ logger.info(f"✅ Stream completed [{request_id}]")
460
+
461
+ except Exception as e:
462
+ logger.error(f"Stream error [{request_id}]: {e}")
463
+
464
+ error_payload = {
465
+ "error": {
466
+ "message": str(e),
467
+ "type": "server_error",
468
+ }
469
+ }
470
+ yield f"data: {json.dumps(error_payload)}\n\n"
471
+ yield "data: [DONE]\n\n"
472
+
473
+ return StreamingResponse(
474
+ generate_stream(),
475
+ media_type="text/event-stream",
476
+ headers={
477
+ "Cache-Control": "no-cache",
478
+ "Connection": "keep-alive",
479
+ "X-Accel-Buffering": "no",
480
+ "X-Request-Id": request_id,
481
+ },
482
+ )
483
+
484
+ # =====================================
485
+ # NORMAL (NON-STREAM) RESPONSE
486
+ # =====================================
487
+
488
  try:
489
+ response, used_model = create_chat(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
490
  messages=messages,
491
+ model=body.model,
492
  stream=False,
493
  )
494
 
495
+ # استخراج الرد
496
+ assistant_message = ""
497
+ try:
498
+ assistant_message = response.choices[0].message.content
499
+ except (AttributeError, IndexError):
500
+ assistant_message = str(response)
501
+
502
+ logger.info(
503
+ f"✅ Response [{request_id}] "
504
+ f"length={len(assistant_message)}"
505
+ )
506
+
507
+ return JSONResponse({
508
+ "id": request_id,
509
+ "object": "chat.completion",
510
+ "created": int(time.time()),
511
+ "model": used_model,
512
+ "choices": [
513
+ {
514
+ "index": 0,
515
+ "message": {
516
+ "role": "assistant",
517
+ "content": assistant_message,
518
+ },
519
+ "finish_reason": "stop",
520
+ }
521
+ ],
522
+ "usage": {
523
+ "prompt_tokens": 0,
524
+ "completion_tokens": 0,
525
+ "total_tokens": 0,
526
+ },
527
  })
528
 
529
  except Exception as e:
530
+ logger.error(f"Chat error [{request_id}]: {e}")
531
+ raise HTTPException(
532
+ status_code=500,
533
+ detail={
534
+ "error": {
535
+ "message": str(e),
536
+ "type": "server_error",
537
+ }
538
+ },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  )
540
 
 
 
541
 
542
+ # =====================================================
543
+ # RUN
544
+ # =====================================================
545
 
546
  if __name__ == "__main__":
547
+ import uvicorn
548
+
549
+ uvicorn.run(
550
+ "app:app",
551
+ host=HOST,
552
+ port=PORT,
553
+ log_level="info",
554
+ reload=False,
555
+ )