bahi-bh commited on
Commit
d589d39
·
verified ·
1 Parent(s): 88ea102

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +273 -393
app.py CHANGED
@@ -1,70 +1,48 @@
1
- """
2
- ╔══════════════════════════════════════════════════════════════════╗
3
- ║ Universal AI Gateway – Production v6.0 ║
4
- ║ ║
5
- ║ • كل نموذج مربوط بـ provider يعمل فعلاً (لا auto عشوائي) ║
6
- ║ • عائلة Cohere كاملة عبر HuggingSpace (بدون auth) ║
7
- ║ • Kimi عبر Jmuz ║
8
- ║ • GPT-4 عبر Jmuz / Liaobots / PollinationsAI ║
9
- ║ • Streaming حقيقي بدون MissingAuthError ║
10
- ║ • Fallback chain مرتب لكل نموذج ║
11
- ╚══════════════════════════════════════════════════════════════════╝
12
- """
13
-
14
- from __future__ import annotations
15
 
16
  import asyncio
17
  import json
18
- import logging
19
  import time
20
  import uuid
21
-
22
- from contextlib import asynccontextmanager
23
- from typing import Any, Dict, List, Optional
24
 
25
  import g4f
26
-
27
- from fastapi import FastAPI, HTTPException, Request
28
- from fastapi.middleware.cors import CORSMiddleware
29
- from fastapi.responses import JSONResponse, StreamingResponse
30
-
31
- from g4f import Provider
32
  from g4f.client import Client
 
33
 
34
- from pydantic import BaseModel
35
 
36
- # ──────────────────────────────────────────────────────────────────
37
  # LOGGING
38
- # ──────────────────────────────────────────────────────────────────
39
 
40
- logging.basicConfig(
41
- level=logging.INFO,
42
- format="%(asctime)s | %(levelname)-7s | %(message)s",
43
- datefmt="%H:%M:%S",
44
- )
45
 
46
- log = logging.getLogger("gw")
47
 
48
- # ──────────────────────────────────────────────────────────────────
49
  # CONFIG
50
- # ──────────────────────────────────────────────────────────────────
51
 
52
  API_KEY = "sk-your-secret-key"
 
 
53
 
54
- CALL_TIMEOUT = 60
55
- STREAM_TIMEOUT = 90
56
 
57
- # ══════════════════════════════════════════════════════════════════
58
- # MODEL → PROVIDER ROUTING TABLE
59
- # ══════════════════════════════════════════════════════════════════
 
 
60
 
61
- ROUTING: Dict[str, List[Any]] = {
62
-
63
- # ── Cohere ────────────────────────────────────────────────
64
  "command-r": [Provider.HuggingSpace, Provider.Jmuz],
65
  "command-r-plus": [Provider.HuggingSpace, Provider.Jmuz],
66
  "command-r7b": [Provider.HuggingSpace],
67
- "command-r7b-arabic": [Provider.HuggingSpace],
68
  "command-a": [Provider.HuggingSpace],
69
  "command": [Provider.HuggingSpace, Provider.Jmuz],
70
  "command-light": [Provider.HuggingSpace, Provider.Jmuz],
@@ -72,476 +50,378 @@ ROUTING: Dict[str, List[Any]] = {
72
  "c4ai-aya-expanse-8b": [Provider.HuggingSpace],
73
  "c4ai-aya-expanse-32b": [Provider.HuggingSpace],
74
 
75
- # ── Kimi / Moonshot ──────────────────────────────────────
76
  "kimi": [Provider.Jmuz],
77
  "moonshot-v1-8k": [Provider.Jmuz],
78
  "moonshot-v1-32k": [Provider.Jmuz],
79
- "moonshot-v1-128k": [Provider.Jmuz],
80
 
81
- # ── GPT ──────────────────────────────────────────────────
82
  "gpt-4": [Provider.Jmuz, Provider.Liaobots, Provider.PollinationsAI],
83
  "gpt-4-turbo": [Provider.Jmuz, Provider.Liaobots, Provider.PollinationsAI],
84
  "gpt-4o": [Provider.PollinationsAI, Provider.Jmuz, Provider.Liaobots],
85
  "gpt-4o-mini": [Provider.PollinationsAI, Provider.DDG, Provider.Jmuz],
86
  "gpt-3.5-turbo": [Provider.DDG, Provider.Jmuz, Provider.PollinationsAI],
87
 
88
- # ── DeepSeek ─────────────────────────────────────────────
89
  "deepseek-chat": [Provider.PollinationsAI, Provider.Jmuz],
90
- "deepseek-r1": [Provider.PollinationsAI, Provider.Jmuz, Provider.DDG],
91
  "deepseek-v3": [Provider.PollinationsAI, Provider.Jmuz],
92
 
93
- # ── Llama ────────────────────────────────────────────────
94
- "llama-3.1-8b": [Provider.PollinationsAI, Provider.Jmuz, Provider.DDG],
95
  "llama-3.1-70b": [Provider.PollinationsAI, Provider.Jmuz],
96
  "llama-3.3-70b": [Provider.PollinationsAI, Provider.Jmuz],
97
- "llama-3.2-11b": [Provider.PollinationsAI],
98
  "llama-4-scout": [Provider.PollinationsAI],
99
  "llama-4-maverick": [Provider.PollinationsAI],
100
 
101
- # ── Mistral ──────────────────────────────────────────────
102
- "mistral-7b": [Provider.PollinationsAI, Provider.Jmuz],
103
- "mixtral-8x7b": [Provider.PollinationsAI, Provider.Jmuz],
104
- "mistral-small": [Provider.PollinationsAI],
105
- "mistral-large": [Provider.PollinationsAI, Provider.Jmuz],
106
 
107
- # ── Gemini ───────────────────────────────────────────────
108
  "gemini-2.0-flash": [Provider.PollinationsAI, Provider.Jmuz],
109
- "gemini-1.5-flash": [Provider.PollinationsAI, Provider.Jmuz],
110
- "gemini-1.5-pro": [Provider.PollinationsAI, Provider.Jmuz],
111
- "gemini-pro": [Provider.PollinationsAI, Provider.Jmuz],
112
 
113
- # ── Qwen ─────────────────────────────────────────────────
114
  "qwen-2.5-72b": [Provider.PollinationsAI, Provider.Jmuz],
115
  "qwen-2.5-coder-32b": [Provider.PollinationsAI],
116
  "qwq-32b": [Provider.PollinationsAI, Provider.Jmuz],
117
 
118
- # ── Claude ───────────────────────────────────────────────
119
  "claude-3-haiku": [Provider.Jmuz, Provider.Liaobots],
120
  "claude-3-sonnet": [Provider.Jmuz, Provider.Liaobots],
121
  "claude-3-opus": [Provider.Jmuz, Provider.Liaobots],
122
  "claude-3.5-sonnet": [Provider.Jmuz, Provider.Liaobots],
123
 
124
- # ── Other ────────────────────────────────────────────────
125
  "phi-4": [Provider.PollinationsAI],
126
  "sonar-pro": [Provider.PollinationsAI, Provider.Jmuz],
127
  "sonar": [Provider.PollinationsAI, Provider.Jmuz],
128
  }
129
 
130
- ALL_MODELS: List[str] = sorted(ROUTING.keys())
131
 
132
- # ══════════════════════════════════════════════════════════════════
133
- # CORE CALL
134
- # ══════════════════════════════════════════════════════════════════
135
 
136
- def _call(provider_cls: Any, model: str, messages: list, stream: bool) -> Any:
 
 
 
137
 
138
- client = Client(provider=provider_cls)
139
 
140
- return client.chat.completions.create(
141
- model=model,
142
- messages=messages,
143
- stream=stream,
144
- )
145
 
146
- # ══════════════════════════════════════════════════════════════════
147
- # SMART COMPLETION
148
- # ══════════════════════════════════════════════════════════════════
 
 
 
 
149
 
150
- async def smart_completion(
151
- model: str,
152
- messages: list,
 
 
 
 
 
 
 
 
 
 
153
  stream: bool = False
154
- ) -> Any:
 
155
 
156
- chain = ROUTING.get(model)
157
 
158
- if not chain:
159
- log.warning(
160
- f"Unknown model '{model}' – using generic fallback chain"
161
- )
162
- chain = [
163
- Provider.PollinationsAI,
164
- Provider.Jmuz,
165
- Provider.DDG,
166
- ]
167
 
168
- timeout = STREAM_TIMEOUT if stream else CALL_TIMEOUT
 
 
 
 
 
 
 
 
 
169
 
170
- errors: List[str] = []
171
 
172
- for provider_cls in chain:
 
 
173
 
174
- pname = getattr(
175
- provider_cls,
176
- "__name__",
177
- str(provider_cls)
178
- )
 
 
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  try:
 
181
 
182
- log.info(
183
- f" -> {pname} | model={model} | stream={stream}"
184
- )
185
 
186
- resp = await asyncio.wait_for(
187
  asyncio.to_thread(
188
- _call,
189
- provider_cls,
190
- model,
191
- messages,
192
- stream,
193
  ),
194
- timeout=timeout,
195
  )
196
 
197
- log.info(f" OK {pname}")
198
-
199
- return resp
200
 
201
  except asyncio.TimeoutError:
 
 
202
 
203
- msg = f"{pname}: timeout after {timeout}s"
204
-
205
- except Exception as exc:
206
-
207
- msg = f"{pname}: {type(exc).__name__}: {exc}"
208
 
209
- log.warning(f" FAIL {msg}")
210
 
211
- errors.append(msg)
212
 
213
- raise RuntimeError(
214
- f"All providers failed for '{model}':\n"
215
- + "\n".join(errors)
216
- )
217
 
218
- # ══════════════════════════════════════════════════════════════════
219
- # STREAMING GENERATOR
220
- # ══════════════════════════════════════════════════════════════════
221
 
222
- async def sse_generator(model: str, messages: list):
223
 
224
- cid = f"chatcmpl-{uuid.uuid4().hex}"
225
- created = int(time.time())
 
 
226
 
227
- sent = False
228
 
229
- try:
 
 
230
 
231
- response = await smart_completion(
232
- model,
233
- messages,
234
- stream=True,
235
- )
236
 
237
- for chunk in response:
238
 
239
  try:
240
 
241
- content = (
242
- chunk.choices[0].delta.content or ""
 
 
243
  )
244
 
245
- except Exception:
246
-
247
- content = ""
248
-
249
- if not content:
250
- continue
251
-
252
- sent = True
253
-
254
- yield (
255
- "data: "
256
- + json.dumps(
257
- {
258
- "id": cid,
259
- "object": "chat.completion.chunk",
260
- "created": created,
261
- "model": model,
262
- "choices": [
263
- {
264
- "index": 0,
265
- "delta": {
266
- "content": content
267
- },
268
- "finish_reason": None,
 
 
 
 
 
 
 
 
 
269
  }
270
- ],
271
- },
272
- ensure_ascii=False,
273
- )
274
- + "\n\n"
275
- )
276
 
277
- await asyncio.sleep(0)
 
 
 
 
 
278
 
279
- if not sent:
 
280
 
281
- yield (
282
- "data: "
283
- + json.dumps(
284
- {
285
  "error": {
286
  "message": "Provider returned empty stream",
287
- "type": "empty_stream",
288
  }
289
  }
290
- )
291
- + "\n\n"
292
- )
293
 
294
- # stop chunk
295
- yield (
296
- "data: "
297
- + json.dumps(
298
- {
299
- "id": cid,
300
  "object": "chat.completion.chunk",
301
- "created": created,
302
- "model": model,
303
  "choices": [
304
  {
305
  "index": 0,
306
  "delta": {},
307
- "finish_reason": "stop",
308
  }
309
- ],
310
  }
311
- )
312
- + "\n\n"
313
- )
314
 
315
- yield "data: [DONE]\n\n"
 
316
 
317
- except Exception as exc:
318
 
319
- log.error(f"Stream error: {exc}")
320
 
321
- yield (
322
- "data: "
323
- + json.dumps(
324
- {
325
  "error": {
326
- "message": str(exc),
327
- "type": "server_error",
328
  }
329
  }
330
- )
331
- + "\n\n"
332
- )
333
 
334
- yield "data: [DONE]\n\n"
335
-
336
- # ══════════════════════════════════════════════════════════════════
337
- # FASTAPI
338
- # ══════════════════════════════════════════════════════════════════
339
-
340
- @asynccontextmanager
341
- async def lifespan(app: FastAPI):
342
-
343
- log.info(
344
- f"Gateway ready — {len(ALL_MODELS)} models | providers pinned (no auto)"
345
- )
346
-
347
- yield
348
-
349
-
350
- app = FastAPI(
351
- title="Universal AI Gateway",
352
- version="6.0.0",
353
- lifespan=lifespan,
354
- )
355
-
356
- app.add_middleware(
357
- CORSMiddleware,
358
- allow_origins=["*"],
359
- allow_credentials=True,
360
- allow_methods=["*"],
361
- allow_headers=["*"],
362
- )
363
-
364
- # ══════════════════════════════════════════════════════════════════
365
- # Pydantic Models
366
- # ══════════════════════════════════════════════════════════════════
367
-
368
- class Message(BaseModel):
369
- role: str
370
- content: str
371
-
372
-
373
- class ChatRequest(BaseModel):
374
- model: str
375
- messages: List[Message]
376
- stream: bool = False
377
- temperature: Optional[float] = 0.7
378
- max_tokens: Optional[int] = 4096
379
-
380
- # ══════════════════════════════════════════════════════════════════
381
- # AUTH
382
- # ══════════════════════════════════════════════════════════════════
383
-
384
- def _auth(req: Request):
385
-
386
- auth = req.headers.get("Authorization", "")
387
-
388
- if not auth:
389
- return
390
-
391
- if not auth.startswith("Bearer "):
392
- raise HTTPException(
393
- 401,
394
- "Invalid Authorization format"
395
- )
396
-
397
- if auth.removeprefix("Bearer ").strip() != API_KEY:
398
- raise HTTPException(
399
- 403,
400
- "Invalid API key"
401
- )
402
-
403
- # ══════════════════════════════════════════════════════════════════
404
- # ROOT
405
- # ══════════════════════════════════════════════════════════════════
406
-
407
- @app.get("/")
408
- async def root():
409
-
410
- return {
411
- "service": "Universal AI Gateway",
412
- "version": "6.0.0",
413
- "models": len(ALL_MODELS),
414
- "docs": "/docs",
415
- }
416
-
417
- # ══════════════════════════════════════════════════════════════════
418
- # MODELS ENDPOINT
419
- # ══════════════════════════════════════════════════════════════════
420
-
421
- @app.get("/v1/models")
422
- async def get_models(req: Request):
423
-
424
- _auth(req)
425
-
426
- now = int(time.time())
427
-
428
- return {
429
- "object": "list",
430
- "data": [
431
- {
432
- "id": m,
433
- "object": "model",
434
- "created": now,
435
- "owned_by": "g4f",
436
- "providers": [
437
- getattr(p, "__name__", str(p))
438
- for p in ROUTING.get(m, [])
439
- ],
440
- }
441
- for m in ALL_MODELS
442
- ],
443
- }
444
-
445
- # ══════════════════════════════════════════════════════════════════
446
- # CHAT COMPLETIONS
447
- # ══════════════════════════════════════════════════════════════════
448
-
449
- @app.post("/v1/chat/completions")
450
- async def chat_completions(
451
- req: Request,
452
- body: ChatRequest
453
- ):
454
-
455
- _auth(req)
456
-
457
- messages = [
458
- {
459
- "role": m.role,
460
- "content": m.content,
461
- }
462
- for m in body.messages
463
- ]
464
-
465
- log.info(
466
- f"Request model={body.model} stream={body.stream}"
467
- )
468
-
469
- # STREAMING
470
- if body.stream:
471
 
472
  return StreamingResponse(
473
- sse_generator(body.model, messages),
474
  media_type="text/event-stream",
475
  headers={
476
  "Cache-Control": "no-cache",
477
  "Connection": "keep-alive",
478
- "X-Accel-Buffering": "no",
479
- },
480
  )
481
 
 
482
  # NORMAL RESPONSE
 
 
483
  try:
484
 
485
- response = await smart_completion(
486
- body.model,
487
- messages,
488
- stream=False,
489
  )
490
 
491
- try:
492
-
493
- content = (
494
- response.choices[0].message.content
495
- )
496
 
 
 
497
  except Exception:
498
-
499
- content = str(response)
500
-
501
- return JSONResponse(
502
- {
503
- "id": f"chatcmpl-{uuid.uuid4().hex}",
504
- "object": "chat.completion",
505
- "created": int(time.time()),
506
- "model": body.model,
507
- "choices": [
508
- {
509
- "index": 0,
510
- "message": {
511
- "role": "assistant",
512
- "content": content,
513
- },
514
- "finish_reason": "stop",
515
- }
516
- ],
517
- "usage": {
518
- "prompt_tokens": 0,
519
- "completion_tokens": 0,
520
- "total_tokens": 0,
521
- },
522
  }
523
- )
524
 
525
- except Exception as exc:
526
 
527
- log.error(f"Error: {exc}")
528
 
529
  raise HTTPException(
530
- 500,
531
- str(exc)
532
  )
533
 
534
- # ══════════════════════════════════════════════════════════════════
 
535
  # RUN
536
- # ══════════════════════════════════════════════════════════════════
537
 
538
  if __name__ == "__main__":
539
-
540
  import uvicorn
541
-
542
- uvicorn.run(
543
- app,
544
- host="0.0.0.0",
545
- port=7860,
546
- log_level="info",
547
- )
 
1
+ from fastapi import FastAPI, Request, HTTPException
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.responses import StreamingResponse, JSONResponse
4
+ from pydantic import BaseModel
5
+ from typing import List, Optional
 
 
 
 
 
 
 
 
 
6
 
7
  import asyncio
8
  import json
 
9
  import time
10
  import uuid
11
+ import logging
 
 
12
 
13
  import g4f
 
 
 
 
 
 
14
  from g4f.client import Client
15
+ from g4f import Provider
16
 
 
17
 
18
+ # =====================================================
19
  # LOGGING
20
+ # =====================================================
21
 
22
+ logging.basicConfig(level=logging.INFO)
23
+ logger = logging.getLogger(__name__)
 
 
 
24
 
 
25
 
26
+ # =====================================================
27
  # CONFIG
28
+ # =====================================================
29
 
30
  API_KEY = "sk-your-secret-key"
31
+ REQUEST_TIMEOUT = 45
32
+ MAX_RETRIES = 2
33
 
 
 
34
 
35
+ # =====================================================
36
+ # MODEL → PROVIDER MAP
37
+ # الإصلاح الوحيد: كل نموذج مربوط بـ provider يعمل
38
+ # بدون API key بدل auto العشوائي
39
+ # =====================================================
40
 
41
+ MODEL_PROVIDERS = {
42
+ # Cohere - تعمل عبر HuggingSpace
 
43
  "command-r": [Provider.HuggingSpace, Provider.Jmuz],
44
  "command-r-plus": [Provider.HuggingSpace, Provider.Jmuz],
45
  "command-r7b": [Provider.HuggingSpace],
 
46
  "command-a": [Provider.HuggingSpace],
47
  "command": [Provider.HuggingSpace, Provider.Jmuz],
48
  "command-light": [Provider.HuggingSpace, Provider.Jmuz],
 
50
  "c4ai-aya-expanse-8b": [Provider.HuggingSpace],
51
  "c4ai-aya-expanse-32b": [Provider.HuggingSpace],
52
 
53
+ # Kimi
54
  "kimi": [Provider.Jmuz],
55
  "moonshot-v1-8k": [Provider.Jmuz],
56
  "moonshot-v1-32k": [Provider.Jmuz],
 
57
 
58
+ # GPT-4
59
  "gpt-4": [Provider.Jmuz, Provider.Liaobots, Provider.PollinationsAI],
60
  "gpt-4-turbo": [Provider.Jmuz, Provider.Liaobots, Provider.PollinationsAI],
61
  "gpt-4o": [Provider.PollinationsAI, Provider.Jmuz, Provider.Liaobots],
62
  "gpt-4o-mini": [Provider.PollinationsAI, Provider.DDG, Provider.Jmuz],
63
  "gpt-3.5-turbo": [Provider.DDG, Provider.Jmuz, Provider.PollinationsAI],
64
 
65
+ # DeepSeek
66
  "deepseek-chat": [Provider.PollinationsAI, Provider.Jmuz],
67
+ "deepseek-r1": [Provider.PollinationsAI, Provider.Jmuz],
68
  "deepseek-v3": [Provider.PollinationsAI, Provider.Jmuz],
69
 
70
+ # Llama
71
+ "llama-3.1-8b": [Provider.PollinationsAI, Provider.DDG],
72
  "llama-3.1-70b": [Provider.PollinationsAI, Provider.Jmuz],
73
  "llama-3.3-70b": [Provider.PollinationsAI, Provider.Jmuz],
 
74
  "llama-4-scout": [Provider.PollinationsAI],
75
  "llama-4-maverick": [Provider.PollinationsAI],
76
 
77
+ # Mistral
78
+ "mistral-7b": [Provider.PollinationsAI, Provider.Jmuz],
79
+ "mixtral-8x7b": [Provider.PollinationsAI, Provider.Jmuz],
80
+ "mistral-large":[Provider.PollinationsAI, Provider.Jmuz],
 
81
 
82
+ # Gemini
83
  "gemini-2.0-flash": [Provider.PollinationsAI, Provider.Jmuz],
84
+ "gemini-1.5-flash": [Provider.PollinationsAI, Provider.Jmuz],
85
+ "gemini-1.5-pro": [Provider.PollinationsAI, Provider.Jmuz],
86
+ "gemini-pro": [Provider.PollinationsAI, Provider.Jmuz],
87
 
88
+ # Qwen
89
  "qwen-2.5-72b": [Provider.PollinationsAI, Provider.Jmuz],
90
  "qwen-2.5-coder-32b": [Provider.PollinationsAI],
91
  "qwq-32b": [Provider.PollinationsAI, Provider.Jmuz],
92
 
93
+ # Claude (عبر Jmuz proxy)
94
  "claude-3-haiku": [Provider.Jmuz, Provider.Liaobots],
95
  "claude-3-sonnet": [Provider.Jmuz, Provider.Liaobots],
96
  "claude-3-opus": [Provider.Jmuz, Provider.Liaobots],
97
  "claude-3.5-sonnet": [Provider.Jmuz, Provider.Liaobots],
98
 
99
+ # Other
100
  "phi-4": [Provider.PollinationsAI],
101
  "sonar-pro": [Provider.PollinationsAI, Provider.Jmuz],
102
  "sonar": [Provider.PollinationsAI, Provider.Jmuz],
103
  }
104
 
 
105
 
106
+ # =====================================================
107
+ # FASTAPI
108
+ # =====================================================
109
 
110
+ app = FastAPI(
111
+ title="Universal AI Gateway",
112
+ version="4.2.0"
113
+ )
114
 
 
115
 
116
+ # =====================================================
117
+ # CORS
118
+ # =====================================================
 
 
119
 
120
+ app.add_middleware(
121
+ CORSMiddleware,
122
+ allow_origins=["*"],
123
+ allow_credentials=True,
124
+ allow_methods=["*"],
125
+ allow_headers=["*"],
126
+ )
127
 
128
+
129
+ # =====================================================
130
+ # MODELS
131
+ # =====================================================
132
+
133
+ class Message(BaseModel):
134
+ role: str
135
+ content: str
136
+
137
+
138
+ class ChatRequest(BaseModel):
139
+ model: str
140
+ messages: List[Message]
141
  stream: bool = False
142
+ temperature: Optional[float] = 0.7
143
+ max_tokens: Optional[int] = 4096
144
 
 
145
 
146
+ # =====================================================
147
+ # AUTH
148
+ # =====================================================
 
 
 
 
 
 
149
 
150
+ def verify_api_key(req: Request):
151
+ auth = req.headers.get("Authorization")
152
+ if not auth:
153
+ return True
154
+ if not auth.startswith("Bearer "):
155
+ raise HTTPException(status_code=401, detail="Invalid Authorization Format")
156
+ token = auth.replace("Bearer ", "").strip()
157
+ if token != API_KEY:
158
+ raise HTTPException(status_code=403, detail="Invalid API Key")
159
+ return True
160
 
 
161
 
162
+ # =====================================================
163
+ # ROOT
164
+ # =====================================================
165
 
166
+ @app.get("/")
167
+ async def root():
168
+ return {
169
+ "status": "online",
170
+ "service": "Universal AI Gateway",
171
+ "version": "4.2.0"
172
+ }
173
 
174
+
175
+ # =====================================================
176
+ # MODELS
177
+ # =====================================================
178
+
179
+ @app.get("/v1/models")
180
+ async def get_models():
181
+ models_data = []
182
+ now = int(time.time())
183
+ for model_name in MODEL_PROVIDERS.keys():
184
+ models_data.append({
185
+ "id": model_name,
186
+ "object": "model",
187
+ "created": now,
188
+ "owned_by": "g4f"
189
+ })
190
+ return {
191
+ "object": "list",
192
+ "data": models_data
193
+ }
194
+
195
+
196
+ # =====================================================
197
+ # SAFE COMPLETION
198
+ # يجرب كل provider في القائمة واحداً تلو الآخر
199
+ # =====================================================
200
+
201
+ async def safe_completion(model, messages, stream=False):
202
+
203
+ # جلب قائمة الـ providers لهذا النموذج
204
+ providers = MODEL_PROVIDERS.get(model)
205
+
206
+ # نموذج غير موجود في الجدول → fallback عام
207
+ if not providers:
208
+ logger.warning(f"Model '{model}' not in table, using fallback providers")
209
+ providers = [Provider.PollinationsAI, Provider.Jmuz, Provider.DDG]
210
+
211
+ last_error = None
212
+
213
+ for provider_cls in providers:
214
+ pname = getattr(provider_cls, "__name__", str(provider_cls))
215
  try:
216
+ logger.info(f"Trying provider={pname} model={model}")
217
 
218
+ client = Client(provider=provider_cls)
 
 
219
 
220
+ response = await asyncio.wait_for(
221
  asyncio.to_thread(
222
+ client.chat.completions.create,
223
+ model=model,
224
+ messages=messages,
225
+ stream=stream
 
226
  ),
227
+ timeout=REQUEST_TIMEOUT
228
  )
229
 
230
+ logger.info(f"Success | provider={pname} model={model}")
231
+ return response
 
232
 
233
  except asyncio.TimeoutError:
234
+ last_error = f"{pname}: timeout"
235
+ logger.warning(f"Timeout | provider={pname}")
236
 
237
+ except Exception as e:
238
+ last_error = e
239
+ logger.warning(f"Failed | provider={pname} | {e}")
 
 
240
 
241
+ raise Exception(last_error)
242
 
 
243
 
244
+ # =====================================================
245
+ # CHAT COMPLETIONS
246
+ # =====================================================
 
247
 
248
+ @app.post("/v1/chat/completions")
249
+ async def chat_completions(req: Request, body: ChatRequest):
 
250
 
251
+ verify_api_key(req)
252
 
253
+ messages = [
254
+ {"role": m.role, "content": m.content}
255
+ for m in body.messages
256
+ ]
257
 
258
+ logger.info(f"Request model={body.model} stream={body.stream}")
259
 
260
+ # =================================================
261
+ # STREAMING
262
+ # =================================================
263
 
264
+ if body.stream:
 
 
 
 
265
 
266
+ async def generate_stream():
267
 
268
  try:
269
 
270
+ response = await safe_completion(
271
+ model=body.model,
272
+ messages=messages,
273
+ stream=True
274
  )
275
 
276
+ chunk_id = f"chatcmpl-{uuid.uuid4().hex}"
277
+ has_content = False
278
+
279
+ for chunk in response:
280
+
281
+ try:
282
+
283
+ content = ""
284
+
285
+ if (
286
+ hasattr(chunk, "choices")
287
+ and chunk.choices
288
+ and chunk.choices[0].delta
289
+ and chunk.choices[0].delta.content
290
+ ):
291
+ content = chunk.choices[0].delta.content
292
+
293
+ if content:
294
+
295
+ has_content = True
296
+
297
+ payload = {
298
+ "id": chunk_id,
299
+ "object": "chat.completion.chunk",
300
+ "created": int(time.time()),
301
+ "model": body.model,
302
+ "choices": [
303
+ {
304
+ "index": 0,
305
+ "delta": {"content": content},
306
+ "finish_reason": None
307
+ }
308
+ ]
309
  }
 
 
 
 
 
 
310
 
311
+ yield (
312
+ f"data: "
313
+ f"{json.dumps(payload, ensure_ascii=False)}\n\n"
314
+ )
315
+
316
+ await asyncio.sleep(0)
317
 
318
+ except Exception as chunk_error:
319
+ logger.error(f"Chunk error: {chunk_error}")
320
 
321
+ if not has_content:
322
+ error_payload = {
 
 
323
  "error": {
324
  "message": "Provider returned empty stream",
325
+ "type": "empty_stream"
326
  }
327
  }
328
+ yield f"data: {json.dumps(error_payload)}\n\n"
 
 
329
 
330
+ final_payload = {
331
+ "id": chunk_id,
 
 
 
 
332
  "object": "chat.completion.chunk",
333
+ "created": int(time.time()),
334
+ "model": body.model,
335
  "choices": [
336
  {
337
  "index": 0,
338
  "delta": {},
339
+ "finish_reason": "stop"
340
  }
341
+ ]
342
  }
 
 
 
343
 
344
+ yield f"data: {json.dumps(final_payload)}\n\n"
345
+ yield "data: [DONE]\n\n"
346
 
347
+ except Exception as e:
348
 
349
+ logger.error(f"Streaming error: {e}")
350
 
351
+ error_payload = {
 
 
 
352
  "error": {
353
+ "message": str(e),
354
+ "type": "server_error"
355
  }
356
  }
 
 
 
357
 
358
+ yield f"data: {json.dumps(error_payload)}\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
 
360
  return StreamingResponse(
361
+ generate_stream(),
362
  media_type="text/event-stream",
363
  headers={
364
  "Cache-Control": "no-cache",
365
  "Connection": "keep-alive",
366
+ "X-Accel-Buffering": "no"
367
+ }
368
  )
369
 
370
+ # =================================================
371
  # NORMAL RESPONSE
372
+ # =================================================
373
+
374
  try:
375
 
376
+ response = await safe_completion(
377
+ model=body.model,
378
+ messages=messages,
379
+ stream=False
380
  )
381
 
382
+ assistant_message = ""
 
 
 
 
383
 
384
+ try:
385
+ assistant_message = response.choices[0].message.content
386
  except Exception:
387
+ assistant_message = str(response)
388
+
389
+ return JSONResponse({
390
+ "id": f"chatcmpl-{uuid.uuid4().hex}",
391
+ "object": "chat.completion",
392
+ "created": int(time.time()),
393
+ "model": body.model,
394
+ "choices": [
395
+ {
396
+ "index": 0,
397
+ "message": {
398
+ "role": "assistant",
399
+ "content": assistant_message
400
+ },
401
+ "finish_reason": "stop"
402
+ }
403
+ ],
404
+ "usage": {
405
+ "prompt_tokens": 0,
406
+ "completion_tokens": 0,
407
+ "total_tokens": 0
 
 
 
408
  }
409
+ })
410
 
411
+ except Exception as e:
412
 
413
+ logger.error(f"Chat error: {e}")
414
 
415
  raise HTTPException(
416
+ status_code=500,
417
+ detail=str(e)
418
  )
419
 
420
+
421
+ # =====================================================
422
  # RUN
423
+ # =====================================================
424
 
425
  if __name__ == "__main__":
 
426
  import uvicorn
427
+ uvicorn.run(app, host="0.0.0.0", port=7860)