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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +327 -97
app.py CHANGED
@@ -1,13 +1,13 @@
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
 
@@ -18,44 +18,49 @@ import json
18
  import logging
19
  import time
20
  import uuid
 
21
  from contextlib import asynccontextmanager
22
  from typing import Any, Dict, List, Optional
23
 
24
  import g4f
 
25
  from fastapi import FastAPI, HTTPException, Request
26
  from fastapi.middleware.cors import CORSMiddleware
27
  from fastapi.responses import JSONResponse, StreamingResponse
 
28
  from g4f import Provider
29
  from g4f.client import Client
 
30
  from pydantic import BaseModel
31
 
32
  # ──────────────────────────────────────────────────────────────────
33
  # LOGGING
34
  # ──────────────────────────────────────────────────────────────────
 
35
  logging.basicConfig(
36
  level=logging.INFO,
37
  format="%(asctime)s | %(levelname)-7s | %(message)s",
38
  datefmt="%H:%M:%S",
39
  )
 
40
  log = logging.getLogger("gw")
41
 
42
  # ──────────────────────────────────────────────────────────────────
43
  # CONFIG
44
  # ──────────────────────────────────────────────────────────────────
45
- API_KEY = "sk-your-secret-key"
46
- CALL_TIMEOUT = 60 # seconds – non-streaming
47
- STREAM_TIMEOUT = 90 # seconds – streaming
48
 
 
 
 
 
49
 
50
  # ══════════════════════════════════════════════════════════════════
51
- # MODEL → PROVIDER ROUTING TABLE
52
- #
53
- # قائمة مرتبة: أول provider ينجح يُستخدم، الباقي fallback.
54
- # لا يوجد أي provider يحتاج API key هنا.
55
  # ══════════════════════════════════════════════════════════════════
 
56
  ROUTING: Dict[str, List[Any]] = {
57
 
58
- # ── Cohere family (HuggingSpace يستضيف spaces رسمية لـ Cohere)
59
  "command-r": [Provider.HuggingSpace, Provider.Jmuz],
60
  "command-r-plus": [Provider.HuggingSpace, Provider.Jmuz],
61
  "command-r7b": [Provider.HuggingSpace],
@@ -67,56 +72,56 @@ ROUTING: Dict[str, List[Any]] = {
67
  "c4ai-aya-expanse-8b": [Provider.HuggingSpace],
68
  "c4ai-aya-expanse-32b": [Provider.HuggingSpace],
69
 
70
- # ── Kimi / Moonshot ───────────────────────────────────────────
71
  "kimi": [Provider.Jmuz],
72
  "moonshot-v1-8k": [Provider.Jmuz],
73
  "moonshot-v1-32k": [Provider.Jmuz],
74
  "moonshot-v1-128k": [Provider.Jmuz],
75
 
76
- # ── GPT-4 family ──────────────────────────────────────────────
77
  "gpt-4": [Provider.Jmuz, Provider.Liaobots, Provider.PollinationsAI],
78
  "gpt-4-turbo": [Provider.Jmuz, Provider.Liaobots, Provider.PollinationsAI],
79
  "gpt-4o": [Provider.PollinationsAI, Provider.Jmuz, Provider.Liaobots],
80
  "gpt-4o-mini": [Provider.PollinationsAI, Provider.DDG, Provider.Jmuz],
81
  "gpt-3.5-turbo": [Provider.DDG, Provider.Jmuz, Provider.PollinationsAI],
82
 
83
- # ── DeepSeek ──────────────────────────────────────────────────
84
  "deepseek-chat": [Provider.PollinationsAI, Provider.Jmuz],
85
  "deepseek-r1": [Provider.PollinationsAI, Provider.Jmuz, Provider.DDG],
86
  "deepseek-v3": [Provider.PollinationsAI, Provider.Jmuz],
87
 
88
- # ── Llama ─────────────────────────────────────────────────────
89
- "llama-3.1-8b": [Provider.PollinationsAI, Provider.Jmuz, Provider.DDG],
90
- "llama-3.1-70b": [Provider.PollinationsAI, Provider.Jmuz],
91
- "llama-3.3-70b": [Provider.PollinationsAI, Provider.Jmuz],
92
- "llama-3.2-11b": [Provider.PollinationsAI],
93
- "llama-4-scout": [Provider.PollinationsAI],
94
- "llama-4-maverick":[Provider.PollinationsAI],
95
 
96
- # ── Mistral ───────────────────────────────────────────────────
97
  "mistral-7b": [Provider.PollinationsAI, Provider.Jmuz],
98
  "mixtral-8x7b": [Provider.PollinationsAI, Provider.Jmuz],
99
  "mistral-small": [Provider.PollinationsAI],
100
  "mistral-large": [Provider.PollinationsAI, Provider.Jmuz],
101
 
102
- # ── Gemini ────────────────────────────────────────────────────
103
  "gemini-2.0-flash": [Provider.PollinationsAI, Provider.Jmuz],
104
  "gemini-1.5-flash": [Provider.PollinationsAI, Provider.Jmuz],
105
  "gemini-1.5-pro": [Provider.PollinationsAI, Provider.Jmuz],
106
  "gemini-pro": [Provider.PollinationsAI, Provider.Jmuz],
107
 
108
- # ── Qwen ──────────────────────────────────────────────────────
109
  "qwen-2.5-72b": [Provider.PollinationsAI, Provider.Jmuz],
110
  "qwen-2.5-coder-32b": [Provider.PollinationsAI],
111
  "qwq-32b": [Provider.PollinationsAI, Provider.Jmuz],
112
 
113
- # ── Claude (عبر Jmuz proxy — لا حاجة لـ Anthropic key) ───────
114
- "claude-3-haiku": [Provider.Jmuz, Provider.Liaobots],
115
- "claude-3-sonnet": [Provider.Jmuz, Provider.Liaobots],
116
- "claude-3-opus": [Provider.Jmuz, Provider.Liaobots],
117
- "claude-3.5-sonnet":[Provider.Jmuz, Provider.Liaobots],
118
 
119
- # ─ Other ─────────────────────────────────────────────────────
120
  "phi-4": [Provider.PollinationsAI],
121
  "sonar-pro": [Provider.PollinationsAI, Provider.Jmuz],
122
  "sonar": [Provider.PollinationsAI, Provider.Jmuz],
@@ -124,194 +129,419 @@ ROUTING: Dict[str, List[Any]] = {
124
 
125
  ALL_MODELS: List[str] = sorted(ROUTING.keys())
126
 
127
-
128
  # ══════════════════════════════════════════════════════════════════
129
- # CORE CALL (sync – runs in thread)
130
  # ══════════════════════════════════════════════════════════════════
 
131
  def _call(provider_cls: Any, model: str, messages: list, stream: bool) -> Any:
 
132
  client = Client(provider=provider_cls)
 
133
  return client.chat.completions.create(
134
  model=model,
135
  messages=messages,
136
  stream=stream,
137
  )
138
 
139
-
140
  # ══════════════════════════════════════════════════════════════════
141
- # SMART COMPLETION (async wrapper with fallback chain)
142
  # ══════════════════════════════════════════════════════════════════
143
- async def smart_completion(model: str, messages: list, stream: bool = False) -> Any:
 
 
 
 
 
 
144
  chain = ROUTING.get(model)
 
145
  if not chain:
146
- log.warning(f"Unknown model '{model}' – using generic fallback chain")
147
- chain = [Provider.PollinationsAI, Provider.Jmuz, Provider.DDG]
 
 
 
 
 
 
148
 
149
  timeout = STREAM_TIMEOUT if stream else CALL_TIMEOUT
 
150
  errors: List[str] = []
151
 
152
  for provider_cls in chain:
153
- pname = getattr(provider_cls, "__name__", str(provider_cls))
 
 
 
 
 
 
154
  try:
155
- log.info(f" -> {pname} | model={model} | stream={stream}")
 
 
 
 
156
  resp = await asyncio.wait_for(
157
- asyncio.to_thread(_call, provider_cls, model, messages, stream),
 
 
 
 
 
 
158
  timeout=timeout,
159
  )
 
160
  log.info(f" OK {pname}")
 
161
  return resp
 
162
  except asyncio.TimeoutError:
 
163
  msg = f"{pname}: timeout after {timeout}s"
 
164
  except Exception as exc:
 
165
  msg = f"{pname}: {type(exc).__name__}: {exc}"
 
166
  log.warning(f" FAIL {msg}")
167
- errors.append(msg)
168
 
169
- raise RuntimeError(f"All providers failed for '{model}':\n" + "\n".join(errors))
170
 
 
 
 
 
171
 
172
  # ══════════════════════════════════════════════════════════════════
173
- # STREAMING GENERATOR
174
  # ══════════════════════════════════════════════════════════════════
 
175
  async def sse_generator(model: str, messages: list):
176
- cid = f"chatcmpl-{uuid.uuid4().hex}"
 
177
  created = int(time.time())
178
- sent = False
 
179
 
180
  try:
181
- response = await smart_completion(model, messages, stream=True)
 
 
 
 
 
 
182
  for chunk in response:
 
183
  try:
184
- content = chunk.choices[0].delta.content or ""
 
 
 
 
185
  except Exception:
 
186
  content = ""
 
187
  if not content:
188
  continue
 
189
  sent = True
190
- yield "data: " + json.dumps({
191
- "id": cid, "object": "chat.completion.chunk",
192
- "created": created, "model": model,
193
- "choices": [{"index": 0, "delta": {"content": content}, "finish_reason": None}],
194
- }, ensure_ascii=False) + "\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  await asyncio.sleep(0)
196
 
197
  if not sent:
198
- yield "data: " + json.dumps(
199
- {"error": {"message": "Provider returned empty stream", "type": "empty_stream"}}
200
- ) + "\n\n"
 
 
 
 
 
 
 
 
 
 
201
 
202
  # stop chunk
203
- yield "data: " + json.dumps({
204
- "id": cid, "object": "chat.completion.chunk",
205
- "created": created, "model": model,
206
- "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}],
207
- }) + "\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  yield "data: [DONE]\n\n"
209
 
210
  except Exception as exc:
 
211
  log.error(f"Stream error: {exc}")
212
- yield "data: " + json.dumps(
213
- {"error": {"message": str(exc), "type": "server_error"}}
214
- ) + "\n\n"
215
- yield "data: [DONE]\n\n"
216
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
 
218
  # ══════════════════════════════════════════════════════════════════
219
- # FASTAPI
220
  # ══════════════════════════════════════════════════════════════════
 
221
  @asynccontextmanager
222
  async def lifespan(app: FastAPI):
223
- log.info(f"Gateway ready — {len(ALL_MODELS)} models | providers pinned (no auto)")
 
 
 
 
224
  yield
225
 
226
 
227
- app = FastAPI(title="Universal AI Gateway", version="6.0.0", lifespan=lifespan)
 
 
 
 
 
228
  app.add_middleware(
229
  CORSMiddleware,
230
- allow_origins=["*"], allow_credentials=True,
231
- allow_methods=["*"], allow_headers=["*"],
 
 
232
  )
233
 
 
 
 
234
 
235
  class Message(BaseModel):
236
  role: str
237
  content: str
238
 
 
239
  class ChatRequest(BaseModel):
240
  model: str
241
  messages: List[Message]
242
  stream: bool = False
243
  temperature: Optional[float] = 0.7
244
- max_tokens: Optional[int] = 4096
245
 
 
 
 
246
 
247
  def _auth(req: Request):
 
248
  auth = req.headers.get("Authorization", "")
 
249
  if not auth:
250
  return
 
251
  if not auth.startswith("Bearer "):
252
- raise HTTPException(401, "Invalid Authorization format")
 
 
 
 
253
  if auth.removeprefix("Bearer ").strip() != API_KEY:
254
- raise HTTPException(403, "Invalid API key")
 
 
 
255
 
 
 
 
256
 
257
  @app.get("/")
258
  async def root():
 
259
  return {
260
  "service": "Universal AI Gateway",
261
  "version": "6.0.0",
262
- "models": len(ALL_MODELS),
263
- "docs": "/docs",
264
  }
265
 
 
 
 
266
 
267
  @app.get("/v1/models")
268
  async def get_models(req: Request):
 
269
  _auth(req)
 
270
  now = int(time.time())
 
271
  return {
272
  "object": "list",
273
  "data": [
274
  {
275
- "id": m, "object": "model", "created": now, "owned_by": "g4f",
276
- "providers": [getattr(p, "__name__", str(p)) for p in ROUTING.get(m, [])],
 
 
 
 
 
 
277
  }
278
  for m in ALL_MODELS
279
  ],
280
  }
281
 
 
 
 
282
 
283
  @app.post("/v1/chat/completions")
284
- async def chat_completions(req: Request, body: ChatRequest):
 
 
 
 
285
  _auth(req)
286
- messages = [{"role": m.role, "content": m.content} for m in body.messages]
287
- log.info(f"Request model={body.model} stream={body.stream}")
288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  if body.stream:
 
290
  return StreamingResponse(
291
  sse_generator(body.model, messages),
292
  media_type="text/event-stream",
293
- headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"},
 
 
 
 
294
  )
295
 
 
296
  try:
297
- response = await smart_completion(body.model, messages, stream=False)
 
 
 
 
 
 
298
  try:
299
- content = response.choices[0].message.content
 
 
 
 
300
  except Exception:
 
301
  content = str(response)
302
- return JSONResponse({
303
- "id": f"chatcmpl-{uuid.uuid4().hex}",
304
- "object": "chat.completion",
305
- "created": int(time.time()),
306
- "model": body.model,
307
- "choices": [{"index": 0, "message": {"role": "assistant", "content": content}, "finish_reason": "stop"}],
308
- "usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
309
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  except Exception as exc:
 
311
  log.error(f"Error: {exc}")
312
- raise HTTPException(500, str(exc))
313
 
 
 
 
 
 
 
 
 
314
 
315
  if __name__ == "__main__":
 
316
  import uvicorn
317
- uvicorn.run(app, host="0.0.0.0", port=7860, log_level="info")
 
 
 
 
 
 
 
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
 
 
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],
 
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],
 
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
+ )