bahi-bh commited on
Commit
7413ced
·
verified ·
1 Parent(s): f07bfda

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +392 -65
app.py CHANGED
@@ -8,6 +8,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, Response
 
11
  from pydantic import BaseModel
12
  import g4f
13
 
@@ -103,10 +104,10 @@ CACHE = TTLCache(max_size=100, ttl_seconds=300)
103
  def get_provider(name: str):
104
  try:
105
  return getattr(g4f.Provider, name)
106
- except:
107
  return None
108
 
109
- # ترتيب المزودات حسب احتمال النجاح (جربناها)
110
  REAL_PROVIDERS = {
111
  "Blackbox": get_provider("Blackbox"),
112
  "DeepSeek": get_provider("DeepSeek"),
@@ -119,7 +120,7 @@ REAL_PROVIDERS = {
119
  REAL_PROVIDERS = {k: v for k, v in REAL_PROVIDERS.items() if v}
120
 
121
  # =====================================================
122
- # MODELS - قائمة نماذج لكل مزود (مصادر موثوقة)
123
  # =====================================================
124
  PROVIDER_MODELS_FALLBACK = {
125
  "Blackbox": ["gpt-4o", "claude-3.5-sonnet", "llama-3.1-70b", "gemini-pro"],
@@ -137,12 +138,10 @@ PROVIDER_MODELS_FALLBACK = {
137
  _PROVIDER_MODEL_CACHE = {}
138
 
139
  def discover_provider_models(provider_obj: Any, provider_name: str) -> List[str]:
140
- # إذا كان هناك cache استخدمه
141
  if provider_name in _PROVIDER_MODEL_CACHE:
142
  return _PROVIDER_MODEL_CACHE[provider_name]
143
-
144
  candidates = []
145
- # حاول اكتشاف النماذج من الكائن
146
  for attr in ("models", "model", "default_model", "available_models", "supported_models"):
147
  try:
148
  if hasattr(provider_obj, attr):
@@ -153,13 +152,12 @@ def discover_provider_models(provider_obj: Any, provider_name: str) -> List[str]
153
  candidates.extend(str(i) for i in val)
154
  elif val:
155
  candidates.append(str(val))
156
- except:
157
  pass
158
- # إذا لم يكتشف شيئاً استخدم fallback
159
  if not candidates:
160
  candidates = PROVIDER_MODELS_FALLBACK.get(provider_name, ["gpt-4o"])
161
-
162
- # إزالة التكرار
163
  seen = set()
164
  unique = [m for m in candidates if not (m in seen or seen.add(m))]
165
  _PROVIDER_MODEL_CACHE[provider_name] = unique
@@ -187,7 +185,7 @@ def clean_stream(chunk):
187
  if 'content' in delta:
188
  return delta['content']
189
  return data.get('content') or data.get('text') or ""
190
- except:
191
  pass
192
  if '\\' in chunk:
193
  chunk = chunk.replace('\\n', '\n')
@@ -201,6 +199,30 @@ def clean_stream(chunk):
201
  logger.warning(f"clean_stream error: {e}")
202
  return ""
203
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  # =====================================================
205
  # CHAT LOGIC - مع fallback ذكي
206
  # =====================================================
@@ -221,25 +243,27 @@ def ask(message: str, history, provider_name: str, model_name: str, stop_flag=No
221
  msgs = []
222
  try:
223
  if history:
224
- if history and isinstance(history[0], dict):
225
  for item in history[-40:]:
226
- if role := item.get("role"):
227
- if content := item.get("content"):
228
- msgs.append({"role": str(role), "content": str(content)})
 
 
 
229
  else:
230
  for item in history[-20:]:
231
  if isinstance(item, (list, tuple)) and len(item) == 2:
232
- if u := item[0]:
233
- msgs.append({"role": "user", "content": str(u)})
234
- if a := item[1]:
235
- msgs.append({"role": "assistant", "content": str(a)})
236
  except Exception as e:
237
  logger.warning(f"History error: {e}")
238
 
239
  msgs.append({"role": "user", "content": message})
240
 
241
- # قائمة المزودات التي سنحاولها (مرتبة حسب احتمال النجاح)
242
- # المزود المطلوب أولاً ثم Blackbox ثم DeepSeek ثم البقية
243
  fallback_providers = [
244
  provider_name,
245
  "Blackbox",
@@ -261,19 +285,17 @@ def ask(message: str, history, provider_name: str, model_name: str, stop_flag=No
261
  logger.info(f"Provider {pname} not available, skipping")
262
  continue
263
 
264
- # الحصول على قائمة النماذج لهذا المزود
265
  models_list = discover_provider_models(pobj, pname)
266
  if not models_list:
267
  logger.warning(f"No models for provider {pname}")
268
  continue
269
 
270
- # ترتيب النماذج: النموذج المطلوب أولاً ثم بقية النماذج
271
  if model_name in models_list:
272
  model_candidates = [model_name] + [m for m in models_list if m != model_name]
273
  else:
274
  model_candidates = models_list
275
 
276
- for m in model_candidates[:10]: # جرب أول 10 نماذج كحد أقصى
277
  try:
278
  logger.info(f"Trying provider {pname} with model {m}")
279
  stream = g4f.ChatCompletion.create(
@@ -307,6 +329,15 @@ def ask(message: str, history, provider_name: str, model_name: str, stop_flag=No
307
  # =====================================================
308
  app = FastAPI(title="G4F Smart Router", description="AI Gateway - متعدد المزودات")
309
 
 
 
 
 
 
 
 
 
 
310
  API_KEY = os.getenv("API_KEY", "mysecretkey123")
311
 
312
  class ChatRequest(BaseModel):
@@ -332,7 +363,10 @@ def verify_api_key(request: Request):
332
  if x_api_key and x_api_key == API_KEY:
333
  return True
334
 
335
- raise HTTPException(status_code=401, detail="Invalid API key. Use 'Authorization: Bearer KEY' or 'X-API-Key: KEY'")
 
 
 
336
 
337
  # =====================================================
338
  # دعم HEAD (لإصلاح 405)
@@ -349,88 +383,325 @@ async def head_health():
349
  async def head_models():
350
  return Response(status_code=200)
351
 
 
 
 
 
 
 
 
 
352
  # =====================================================
353
- # نقاط نهاية متوافقة مع Claude Desktop
354
  # =====================================================
355
  @app.get("/v1/models")
356
  async def v1_models(request: Request):
357
- # لا تحتاج مفتاح لهذه النقطة (Claude Desktop يطلبها أولاً)
358
  models = []
 
359
  for pname, pobj in REAL_PROVIDERS.items():
360
  models_list = discover_provider_models(pobj, pname)
361
- for model in models_list[:5]: # نعرض أول 5 نماذج لكل مزود
362
- models.append({
363
- "id": model,
364
- "type": "model",
365
- "display_name": f"{pname} - {model}"
366
- })
 
 
 
 
 
367
  if not models:
368
- models = [{"id": "gpt-4o", "type": "model", "display_name": "Default"}]
369
- return {"data": models}
 
 
 
 
 
 
 
370
 
 
 
 
 
371
  @app.post("/v1/messages")
372
  async def v1_messages(request: Request):
373
  verify_api_key(request)
374
  body = await request.json()
 
375
  messages = body.get("messages", [])
376
  if not messages:
377
  raise HTTPException(status_code=400, detail="No messages provided")
378
- last_message = messages[-1]
379
- user_message = last_message.get("content", "")
380
  model = body.get("model", "gpt-4o")
381
  system_prompt = body.get("system", "")
 
 
 
 
 
 
 
 
382
  history = []
383
  for msg in messages[:-1]:
384
  role = msg.get("role", "user")
385
- content = msg.get("content", "")
386
- history.append({"role": role, "content": content})
 
 
 
387
  full_message = user_message
388
  if system_prompt:
389
  full_message = f"[System: {system_prompt}]\n\n{user_message}"
390
-
 
 
 
 
 
391
  full_response = ""
392
  for chunk in ask(full_message, history, "Blackbox", model):
393
- full_response = chunk
394
-
 
 
 
 
395
  return {
396
- "id": f"msg_{int(time.time())}_{os.urandom(4).hex()}",
397
  "type": "message",
398
  "role": "assistant",
399
  "content": [{"type": "text", "text": full_response}],
400
  "model": model,
401
  "stop_reason": "end_turn",
402
  "stop_sequence": None,
403
- "usage": {"input_tokens": len(user_message)//4, "output_tokens": len(full_response)//4}
 
 
 
404
  }
405
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  @app.post("/v1/messages/stream")
407
  async def v1_messages_stream(request: Request):
408
  verify_api_key(request)
409
  body = await request.json()
 
410
  messages = body.get("messages", [])
411
  if not messages:
412
  raise HTTPException(status_code=400, detail="No messages provided")
 
413
  last_message = messages[-1]
414
- user_message = last_message.get("content", "")
415
  model = body.get("model", "gpt-4o")
416
  system_prompt = body.get("system", "")
 
 
 
 
 
 
 
 
 
 
417
  full_message = user_message
418
  if system_prompt:
419
  full_message = f"[System: {system_prompt}]\n\n{user_message}"
420
-
421
- async def generate_stream():
422
- message_id = f"msg_{int(time.time())}_{os.urandom(4).hex()}"
423
- 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"
424
- yield f"event: content_block_start\ndata: {{\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {{\"type\": \"text\", \"text\": \"\"}}}}\n\n"
425
- for chunk in ask(full_message, [], "Blackbox", model):
426
- 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"
427
- yield f"event: message_delta\ndata: {{\"type\": \"message_delta\", \"delta\": {{\"stop_reason\": \"end_turn\", \"stop_sequence\": null}}, \"usage\": {{\"output_tokens\": 100}}}}\n\n"
428
- yield f"event: message_stop\ndata: {{}}\n\n"
429
-
430
- return StreamingResponse(generate_stream(), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "Connection": "keep-alive"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
 
432
  # =====================================================
433
- # نقاط نهاية إضافية للتوافق القديم
434
  # =====================================================
435
  @app.get("/")
436
  async def root():
@@ -439,10 +710,13 @@ async def root():
439
  "providers": list(REAL_PROVIDERS.keys()),
440
  "endpoints": {
441
  "GET /": "Home",
442
- "GET /health": "Health",
443
  "GET /v1/models": "List models (NO AUTH)",
444
- "POST /v1/messages": "Send message (AUTH)",
445
- "POST /v1/messages/stream": "Stream (AUTH)",
 
 
 
446
  "GET /providers": "Providers list (AUTH)",
447
  },
448
  "cookies": COOKIE_STATUS,
@@ -451,29 +725,79 @@ async def root():
451
 
452
  @app.get("/health")
453
  async def health():
454
- return {"status": "ok", "cookies": COOKIE_STATUS, "providers": list(REAL_PROVIDERS.keys())}
 
 
 
 
 
 
455
 
456
  @app.get("/providers")
457
  async def get_providers(request: Request):
458
  verify_api_key(request)
459
- return {"providers": list(REAL_PROVIDERS.keys())}
 
 
 
 
 
 
 
460
 
461
  @app.post("/chat")
462
  async def chat(request: Request, chat_req: ChatRequest):
463
  verify_api_key(request)
464
  result = ""
465
  for chunk in ask(chat_req.message, chat_req.history, chat_req.provider, chat_req.model):
466
- result = chunk
467
  return JSONResponse({"response": result})
468
 
469
  @app.post("/chat/stream")
470
  async def chat_stream(request: Request, chat_req: ChatRequest):
471
  verify_api_key(request)
 
472
  async def generate():
473
  for chunk in ask(chat_req.message, chat_req.history, chat_req.provider, chat_req.model):
474
  yield f"data: {json.dumps({'delta': chunk}, ensure_ascii=False)}\n\n"
475
  yield "data: [DONE]\n\n"
476
- return StreamingResponse(generate(), media_type="text/event-stream")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
 
478
  # =====================================================
479
  # التشغيل
@@ -481,4 +805,7 @@ async def chat_stream(request: Request, chat_req: ChatRequest):
481
  if __name__ == "__main__":
482
  import uvicorn
483
  port = int(os.getenv("PORT", 7860))
484
- uvicorn.run("app:app", host="0.0.0.0", port=port, reload=False)
 
 
 
 
8
  from collections import OrderedDict
9
  from fastapi import FastAPI, HTTPException, Request
10
  from fastapi.responses import StreamingResponse, JSONResponse, Response
11
+ from fastapi.middleware.cors import CORSMiddleware
12
  from pydantic import BaseModel
13
  import g4f
14
 
 
104
  def get_provider(name: str):
105
  try:
106
  return getattr(g4f.Provider, name)
107
+ except Exception:
108
  return None
109
 
110
+ # ترتيب المزودات حسب احتمال النجاح
111
  REAL_PROVIDERS = {
112
  "Blackbox": get_provider("Blackbox"),
113
  "DeepSeek": get_provider("DeepSeek"),
 
120
  REAL_PROVIDERS = {k: v for k, v in REAL_PROVIDERS.items() if v}
121
 
122
  # =====================================================
123
+ # MODELS - قائمة نماذج لكل مزود
124
  # =====================================================
125
  PROVIDER_MODELS_FALLBACK = {
126
  "Blackbox": ["gpt-4o", "claude-3.5-sonnet", "llama-3.1-70b", "gemini-pro"],
 
138
  _PROVIDER_MODEL_CACHE = {}
139
 
140
  def discover_provider_models(provider_obj: Any, provider_name: str) -> List[str]:
 
141
  if provider_name in _PROVIDER_MODEL_CACHE:
142
  return _PROVIDER_MODEL_CACHE[provider_name]
143
+
144
  candidates = []
 
145
  for attr in ("models", "model", "default_model", "available_models", "supported_models"):
146
  try:
147
  if hasattr(provider_obj, attr):
 
152
  candidates.extend(str(i) for i in val)
153
  elif val:
154
  candidates.append(str(val))
155
+ except Exception:
156
  pass
157
+
158
  if not candidates:
159
  candidates = PROVIDER_MODELS_FALLBACK.get(provider_name, ["gpt-4o"])
160
+
 
161
  seen = set()
162
  unique = [m for m in candidates if not (m in seen or seen.add(m))]
163
  _PROVIDER_MODEL_CACHE[provider_name] = unique
 
185
  if 'content' in delta:
186
  return delta['content']
187
  return data.get('content') or data.get('text') or ""
188
+ except Exception:
189
  pass
190
  if '\\' in chunk:
191
  chunk = chunk.replace('\\n', '\n')
 
199
  logger.warning(f"clean_stream error: {e}")
200
  return ""
201
 
202
+ # =====================================================
203
+ # استخراج النص من content (يدعم string و list)
204
+ # إصلاح: Claude يرسل content كـ list من الكائنات
205
+ # =====================================================
206
+ def extract_text_from_content(content) -> str:
207
+ if isinstance(content, str):
208
+ return content
209
+ if isinstance(content, list):
210
+ parts = []
211
+ for item in content:
212
+ if isinstance(item, str):
213
+ parts.append(item)
214
+ elif isinstance(item, dict):
215
+ if item.get("type") == "text":
216
+ parts.append(item.get("text", ""))
217
+ elif "text" in item:
218
+ parts.append(item["text"])
219
+ elif "content" in item:
220
+ parts.append(str(item["content"]))
221
+ return "\n".join(parts)
222
+ if content is not None:
223
+ return str(content)
224
+ return ""
225
+
226
  # =====================================================
227
  # CHAT LOGIC - مع fallback ذكي
228
  # =====================================================
 
243
  msgs = []
244
  try:
245
  if history:
246
+ if isinstance(history[0], dict):
247
  for item in history[-40:]:
248
+ role = item.get("role")
249
+ content = item.get("content")
250
+ if role and content:
251
+ text = extract_text_from_content(content)
252
+ if text:
253
+ msgs.append({"role": str(role), "content": text})
254
  else:
255
  for item in history[-20:]:
256
  if isinstance(item, (list, tuple)) and len(item) == 2:
257
+ if item[0]:
258
+ msgs.append({"role": "user", "content": str(item[0])})
259
+ if item[1]:
260
+ msgs.append({"role": "assistant", "content": str(item[1])})
261
  except Exception as e:
262
  logger.warning(f"History error: {e}")
263
 
264
  msgs.append({"role": "user", "content": message})
265
 
266
+ # قائمة المزودات التي سنحاولها
 
267
  fallback_providers = [
268
  provider_name,
269
  "Blackbox",
 
285
  logger.info(f"Provider {pname} not available, skipping")
286
  continue
287
 
 
288
  models_list = discover_provider_models(pobj, pname)
289
  if not models_list:
290
  logger.warning(f"No models for provider {pname}")
291
  continue
292
 
 
293
  if model_name in models_list:
294
  model_candidates = [model_name] + [m for m in models_list if m != model_name]
295
  else:
296
  model_candidates = models_list
297
 
298
+ for m in model_candidates[:10]:
299
  try:
300
  logger.info(f"Trying provider {pname} with model {m}")
301
  stream = g4f.ChatCompletion.create(
 
329
  # =====================================================
330
  app = FastAPI(title="G4F Smart Router", description="AI Gateway - متعدد المزودات")
331
 
332
+ # إضافة CORS للسماح بالطلبات من أي مصدر
333
+ app.add_middleware(
334
+ CORSMiddleware,
335
+ allow_origins=["*"],
336
+ allow_credentials=True,
337
+ allow_methods=["*"],
338
+ allow_headers=["*"],
339
+ )
340
+
341
  API_KEY = os.getenv("API_KEY", "mysecretkey123")
342
 
343
  class ChatRequest(BaseModel):
 
363
  if x_api_key and x_api_key == API_KEY:
364
  return True
365
 
366
+ raise HTTPException(
367
+ status_code=401,
368
+ detail="Invalid API key. Use 'Authorization: Bearer KEY' or 'X-API-Key: KEY'"
369
+ )
370
 
371
  # =====================================================
372
  # دعم HEAD (لإصلاح 405)
 
383
  async def head_models():
384
  return Response(status_code=200)
385
 
386
+ @app.head("/v1/messages")
387
+ async def head_messages():
388
+ return Response(status_code=200)
389
+
390
+ @app.head("/v1/chat/completions")
391
+ async def head_chat_completions():
392
+ return Response(status_code=200)
393
+
394
  # =====================================================
395
+ # نقاط نهاية متوافقة مع Claude Coowork و Anthropic API
396
  # =====================================================
397
  @app.get("/v1/models")
398
  async def v1_models(request: Request):
 
399
  models = []
400
+ seen_ids = set()
401
  for pname, pobj in REAL_PROVIDERS.items():
402
  models_list = discover_provider_models(pobj, pname)
403
+ for model in models_list[:5]:
404
+ if model not in seen_ids:
405
+ seen_ids.add(model)
406
+ models.append({
407
+ "id": model,
408
+ "object": "model",
409
+ "created": int(time.time()),
410
+ "owned_by": pname,
411
+ "type": "model",
412
+ "display_name": f"{pname} - {model}"
413
+ })
414
  if not models:
415
+ models = [{
416
+ "id": "gpt-4o",
417
+ "object": "model",
418
+ "created": int(time.time()),
419
+ "owned_by": "system",
420
+ "type": "model",
421
+ "display_name": "Default"
422
+ }]
423
+ return {"object": "list", "data": models}
424
 
425
+ # =====================================================
426
+ # إصلاح: نقطة /v1/messages تدعم stream و non-stream
427
+ # Claude Coowork يرسل stream: true في body نفسه
428
+ # =====================================================
429
  @app.post("/v1/messages")
430
  async def v1_messages(request: Request):
431
  verify_api_key(request)
432
  body = await request.json()
433
+
434
  messages = body.get("messages", [])
435
  if not messages:
436
  raise HTTPException(status_code=400, detail="No messages provided")
437
+
438
+ # استخراج النموذج
439
  model = body.get("model", "gpt-4o")
440
  system_prompt = body.get("system", "")
441
+ is_stream = body.get("stream", False)
442
+ max_tokens = body.get("max_tokens", 4096)
443
+
444
+ # استخراج آخر رسالة مستخدم
445
+ last_message = messages[-1]
446
+ user_message = extract_text_from_content(last_message.get("content", ""))
447
+
448
+ # بناء history من كل الرسائل ما عدا الأخيرة
449
  history = []
450
  for msg in messages[:-1]:
451
  role = msg.get("role", "user")
452
+ content = extract_text_from_content(msg.get("content", ""))
453
+ if role and content:
454
+ history.append({"role": role, "content": content})
455
+
456
+ # إضافة system prompt إذا وجد
457
  full_message = user_message
458
  if system_prompt:
459
  full_message = f"[System: {system_prompt}]\n\n{user_message}"
460
+
461
+ # إذا كان stream مطلوب
462
+ if is_stream:
463
+ return await _handle_anthropic_stream(full_message, history, model, max_tokens)
464
+
465
+ # وضع non-stream: تجميع الرد كاملاً
466
  full_response = ""
467
  for chunk in ask(full_message, history, "Blackbox", model):
468
+ full_response += chunk # إصلاح: += بدلاً من =
469
+
470
+ message_id = f"msg_{int(time.time())}_{os.urandom(4).hex()}"
471
+ input_tokens = max(1, len(user_message) // 4)
472
+ output_tokens = max(1, len(full_response) // 4)
473
+
474
  return {
475
+ "id": message_id,
476
  "type": "message",
477
  "role": "assistant",
478
  "content": [{"type": "text", "text": full_response}],
479
  "model": model,
480
  "stop_reason": "end_turn",
481
  "stop_sequence": None,
482
+ "usage": {
483
+ "input_tokens": input_tokens,
484
+ "output_tokens": output_tokens
485
+ }
486
  }
487
 
488
+ # =====================================================
489
+ # معالج الـ streaming بصيغة Anthropic SSE
490
+ # =====================================================
491
+ async def _handle_anthropic_stream(full_message: str, history: list, model: str, max_tokens: int):
492
+ async def generate_stream():
493
+ message_id = f"msg_{int(time.time())}_{os.urandom(4).hex()}"
494
+
495
+ # حدث بداية الرسالة
496
+ msg_start = {
497
+ "type": "message_start",
498
+ "message": {
499
+ "id": message_id,
500
+ "type": "message",
501
+ "role": "assistant",
502
+ "content": [],
503
+ "model": model,
504
+ "stop_reason": None,
505
+ "stop_sequence": None,
506
+ "usage": {
507
+ "input_tokens": max(1, len(full_message) // 4),
508
+ "output_tokens": 0
509
+ }
510
+ }
511
+ }
512
+ yield f"event: message_start\ndata: {json.dumps(msg_start, ensure_ascii=False)}\n\n"
513
+
514
+ # حدث بداية كتلة المحتوى
515
+ block_start = {
516
+ "type": "content_block_start",
517
+ "index": 0,
518
+ "content_block": {
519
+ "type": "text",
520
+ "text": ""
521
+ }
522
+ }
523
+ yield f"event: content_block_start\ndata: {json.dumps(block_start, ensure_ascii=False)}\n\n"
524
+
525
+ # إرسال أجزاء النص
526
+ output_tokens = 0
527
+ for chunk in ask(full_message, history, "Blackbox", model):
528
+ if chunk:
529
+ output_tokens += max(1, len(chunk) // 4)
530
+ delta_event = {
531
+ "type": "content_block_delta",
532
+ "index": 0,
533
+ "delta": {
534
+ "type": "text_delta",
535
+ "text": chunk
536
+ }
537
+ }
538
+ yield f"event: content_block_delta\ndata: {json.dumps(delta_event, ensure_ascii=False)}\n\n"
539
+
540
+ # حدث نهاية كتلة المحتوى
541
+ block_stop = {
542
+ "type": "content_block_stop",
543
+ "index": 0
544
+ }
545
+ yield f"event: content_block_stop\ndata: {json.dumps(block_stop, ensure_ascii=False)}\n\n"
546
+
547
+ # حدث دلتا الرسالة (نهاية)
548
+ msg_delta = {
549
+ "type": "message_delta",
550
+ "delta": {
551
+ "stop_reason": "end_turn",
552
+ "stop_sequence": None
553
+ },
554
+ "usage": {
555
+ "output_tokens": output_tokens
556
+ }
557
+ }
558
+ yield f"event: message_delta\ndata: {json.dumps(msg_delta, ensure_ascii=False)}\n\n"
559
+
560
+ # حدث توقف الرسالة
561
+ yield f"event: message_stop\ndata: {{\"type\": \"message_stop\"}}\n\n"
562
+
563
+ return StreamingResponse(
564
+ generate_stream(),
565
+ media_type="text/event-stream",
566
+ headers={
567
+ "Cache-Control": "no-cache",
568
+ "Connection": "keep-alive",
569
+ "X-Accel-Buffering": "no",
570
+ }
571
+ )
572
+
573
+ # =====================================================
574
+ # إصلاح: نقطة /v1/messages/stream منفصلة (للتوافق)
575
+ # =====================================================
576
  @app.post("/v1/messages/stream")
577
  async def v1_messages_stream(request: Request):
578
  verify_api_key(request)
579
  body = await request.json()
580
+
581
  messages = body.get("messages", [])
582
  if not messages:
583
  raise HTTPException(status_code=400, detail="No messages provided")
584
+
585
  last_message = messages[-1]
586
+ user_message = extract_text_from_content(last_message.get("content", ""))
587
  model = body.get("model", "gpt-4o")
588
  system_prompt = body.get("system", "")
589
+ max_tokens = body.get("max_tokens", 4096)
590
+
591
+ # بناء history
592
+ history = []
593
+ for msg in messages[:-1]:
594
+ role = msg.get("role", "user")
595
+ content = extract_text_from_content(msg.get("content", ""))
596
+ if role and content:
597
+ history.append({"role": role, "content": content})
598
+
599
  full_message = user_message
600
  if system_prompt:
601
  full_message = f"[System: {system_prompt}]\n\n{user_message}"
602
+
603
+ return await _handle_anthropic_stream(full_message, history, model, max_tokens)
604
+
605
+ # =====================================================
606
+ # إضافة: نقطة /v1/chat/completions (صيغة OpenAI)
607
+ # بعض العملاء يستخدمون هذه الصيغة
608
+ # =====================================================
609
+ @app.post("/v1/chat/completions")
610
+ async def v1_chat_completions(request: Request):
611
+ verify_api_key(request)
612
+ body = await request.json()
613
+
614
+ messages = body.get("messages", [])
615
+ if not messages:
616
+ raise HTTPException(status_code=400, detail="No messages provided")
617
+
618
+ model = body.get("model", "gpt-4o")
619
+ is_stream = body.get("stream", False)
620
+
621
+ # استخراج آخر رسالة
622
+ last_message = messages[-1]
623
+ user_message = extract_text_from_content(last_message.get("content", ""))
624
+
625
+ # بناء history
626
+ history = []
627
+ for msg in messages[:-1]:
628
+ role = msg.get("role", "user")
629
+ content = extract_text_from_content(msg.get("content", ""))
630
+ if role and content:
631
+ history.append({"role": role, "content": content})
632
+
633
+ completion_id = f"chatcmpl-{int(time.time())}_{os.urandom(4).hex()}"
634
+
635
+ if is_stream:
636
+ # OpenAI streaming format
637
+ async def openai_stream():
638
+ for chunk in ask(user_message, history, "Blackbox", model):
639
+ if chunk:
640
+ data = {
641
+ "id": completion_id,
642
+ "object": "chat.completion.chunk",
643
+ "created": int(time.time()),
644
+ "model": model,
645
+ "choices": [{
646
+ "index": 0,
647
+ "delta": {"content": chunk},
648
+ "finish_reason": None
649
+ }]
650
+ }
651
+ yield f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
652
+
653
+ # إرسال حدث النهاية
654
+ final_data = {
655
+ "id": completion_id,
656
+ "object": "chat.completion.chunk",
657
+ "created": int(time.time()),
658
+ "model": model,
659
+ "choices": [{
660
+ "index": 0,
661
+ "delta": {},
662
+ "finish_reason": "stop"
663
+ }]
664
+ }
665
+ yield f"data: {json.dumps(final_data, ensure_ascii=False)}\n\n"
666
+ yield "data: [DONE]\n\n"
667
+
668
+ return StreamingResponse(
669
+ openai_stream(),
670
+ media_type="text/event-stream",
671
+ headers={
672
+ "Cache-Control": "no-cache",
673
+ "Connection": "keep-alive",
674
+ "X-Accel-Buffering": "no",
675
+ }
676
+ )
677
+
678
+ # Non-stream OpenAI format
679
+ full_response = ""
680
+ for chunk in ask(user_message, history, "Blackbox", model):
681
+ full_response += chunk
682
+
683
+ return {
684
+ "id": completion_id,
685
+ "object": "chat.completion",
686
+ "created": int(time.time()),
687
+ "model": model,
688
+ "choices": [{
689
+ "index": 0,
690
+ "message": {
691
+ "role": "assistant",
692
+ "content": full_response
693
+ },
694
+ "finish_reason": "stop"
695
+ }],
696
+ "usage": {
697
+ "prompt_tokens": max(1, len(user_message) // 4),
698
+ "completion_tokens": max(1, len(full_response) // 4),
699
+ "total_tokens": max(1, (len(user_message) + len(full_response)) // 4)
700
+ }
701
+ }
702
 
703
  # =====================================================
704
+ # نقاط نهاية إضافية
705
  # =====================================================
706
  @app.get("/")
707
  async def root():
 
710
  "providers": list(REAL_PROVIDERS.keys()),
711
  "endpoints": {
712
  "GET /": "Home",
713
+ "GET /health": "Health check",
714
  "GET /v1/models": "List models (NO AUTH)",
715
+ "POST /v1/messages": "Anthropic format - Send message (AUTH, supports stream:true)",
716
+ "POST /v1/messages/stream": "Anthropic format - Stream only (AUTH)",
717
+ "POST /v1/chat/completions": "OpenAI format - Send message (AUTH, supports stream:true)",
718
+ "POST /chat": "Simple chat (AUTH)",
719
+ "POST /chat/stream": "Simple stream (AUTH)",
720
  "GET /providers": "Providers list (AUTH)",
721
  },
722
  "cookies": COOKIE_STATUS,
 
725
 
726
  @app.get("/health")
727
  async def health():
728
+ return {
729
+ "status": "ok",
730
+ "cookies": COOKIE_STATUS,
731
+ "providers": list(REAL_PROVIDERS.keys()),
732
+ "provider_count": len(REAL_PROVIDERS),
733
+ "timestamp": int(time.time())
734
+ }
735
 
736
  @app.get("/providers")
737
  async def get_providers(request: Request):
738
  verify_api_key(request)
739
+ result = {}
740
+ for pname, pobj in REAL_PROVIDERS.items():
741
+ models = discover_provider_models(pobj, pname)
742
+ result[pname] = {
743
+ "available": True,
744
+ "models": models
745
+ }
746
+ return {"providers": result}
747
 
748
  @app.post("/chat")
749
  async def chat(request: Request, chat_req: ChatRequest):
750
  verify_api_key(request)
751
  result = ""
752
  for chunk in ask(chat_req.message, chat_req.history, chat_req.provider, chat_req.model):
753
+ result += chunk # إصلاح: += بدلاً من =
754
  return JSONResponse({"response": result})
755
 
756
  @app.post("/chat/stream")
757
  async def chat_stream(request: Request, chat_req: ChatRequest):
758
  verify_api_key(request)
759
+
760
  async def generate():
761
  for chunk in ask(chat_req.message, chat_req.history, chat_req.provider, chat_req.model):
762
  yield f"data: {json.dumps({'delta': chunk}, ensure_ascii=False)}\n\n"
763
  yield "data: [DONE]\n\n"
764
+
765
+ return StreamingResponse(
766
+ generate(),
767
+ media_type="text/event-stream",
768
+ headers={
769
+ "Cache-Control": "no-cache",
770
+ "Connection": "keep-alive",
771
+ }
772
+ )
773
+
774
+ # =====================================================
775
+ # معالجة الأخطاء العامة
776
+ # =====================================================
777
+ @app.exception_handler(404)
778
+ async def not_found_handler(request: Request, exc: HTTPException):
779
+ return JSONResponse(
780
+ status_code=404,
781
+ content={
782
+ "error": {
783
+ "type": "not_found_error",
784
+ "message": f"Endpoint {request.method} {request.url.path} not found"
785
+ }
786
+ }
787
+ )
788
+
789
+ @app.exception_handler(500)
790
+ async def internal_error_handler(request: Request, exc: Exception):
791
+ logger.error(f"Internal error: {str(exc)}")
792
+ return JSONResponse(
793
+ status_code=500,
794
+ content={
795
+ "error": {
796
+ "type": "api_error",
797
+ "message": "Internal server error"
798
+ }
799
+ }
800
+ )
801
 
802
  # =====================================================
803
  # التشغيل
 
805
  if __name__ == "__main__":
806
  import uvicorn
807
  port = int(os.getenv("PORT", 7860))
808
+ logger.info(f"Starting G4F Smart Router on port {port}")
809
+ logger.info(f"Cookies: {COOKIE_STATUS}")
810
+ logger.info(f"Available providers: {list(REAL_PROVIDERS.keys())}")
811
+ uvicorn.run("app:app", host="0.0.0.0", port=port, reload=False)