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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1202 -92
app.py CHANGED
@@ -1,10 +1,13 @@
 
1
  import os
2
  import json
3
  import time
4
  import logging
5
  import asyncio
6
  import threading
7
- from typing import Any, Dict, List, Optional
 
 
8
  from collections import OrderedDict
9
  from fastapi import FastAPI, HTTPException, Request
10
  from fastapi.responses import StreamingResponse, JSONResponse, Response
@@ -214,6 +217,26 @@ def extract_text_from_content(content) -> str:
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:
@@ -223,6 +246,766 @@ def extract_text_from_content(content) -> str:
223
  return str(content)
224
  return ""
225
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
  # =====================================================
227
  # CHAT LOGIC - مع fallback ذكي
228
  # =====================================================
@@ -233,7 +1016,7 @@ def ask(message: str, history, provider_name: str, model_name: str, stop_flag=No
233
  return
234
 
235
  # مفتاح التخزين المؤقت
236
- key = f"{provider_name}|{model_name}|{message}"
237
  cached = CACHE.get(key)
238
  if cached:
239
  yield cached
@@ -243,21 +1026,22 @@ def ask(message: str, history, provider_name: str, model_name: str, stop_flag=No
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
 
@@ -327,7 +1111,7 @@ def ask(message: str, history, provider_name: str, model_name: str, stop_flag=No
327
  # =====================================================
328
  # FASTAPI
329
  # =====================================================
330
- app = FastAPI(title="G4F Smart Router", description="AI Gateway - متعدد المزودات")
331
 
332
  # إضافة CORS للسماح بالطلبات من أي مصدر
333
  app.add_middleware(
@@ -392,7 +1176,7 @@ 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):
@@ -423,61 +1207,108 @@ async def v1_models(request: Request):
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,
@@ -487,10 +1318,20 @@ async def v1_messages(request: Request):
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 = {
@@ -511,44 +1352,176 @@ async def _handle_anthropic_stream(full_message: str, history: list, model: str,
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": {
@@ -571,7 +1544,7 @@ async def _handle_anthropic_stream(full_message: str, history: list, model: str,
571
  )
572
 
573
  # =====================================================
574
- # إصلاح: نقطة /v1/messages/stream منفصلة (للتوافق)
575
  # =====================================================
576
  @app.post("/v1/messages/stream")
577
  async def v1_messages_stream(request: Request):
@@ -582,29 +1555,30 @@ async def v1_messages_stream(request: Request):
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):
@@ -706,18 +1680,27 @@ async def v1_chat_completions(request: Request):
706
  @app.get("/")
707
  async def root():
708
  return {
709
- "message": "G4F Smart Router is running (Multi-Provider)",
 
 
 
 
 
 
 
 
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,
723
  "status": "✅ Server is working"
@@ -727,9 +1710,15 @@ async def root():
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
 
@@ -745,12 +1734,91 @@ async def get_providers(request: Request):
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")
@@ -772,13 +1840,14 @@ async def chat_stream(request: Request, chat_req: ChatRequest):
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"
@@ -792,6 +1861,7 @@ async def internal_error_handler(request: Request, exc: Exception):
792
  return JSONResponse(
793
  status_code=500,
794
  content={
 
795
  "error": {
796
  "type": "api_error",
797
  "message": "Internal server error"
@@ -799,13 +1869,53 @@ async def internal_error_handler(request: Request, exc: Exception):
799
  }
800
  )
801
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
802
  # =====================================================
803
  # التشغيل
804
  # =====================================================
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)
 
1
+
2
  import os
3
  import json
4
  import time
5
  import logging
6
  import asyncio
7
  import threading
8
+ import re
9
+ import uuid
10
+ from typing import Any, Dict, List, Optional, Tuple
11
  from collections import OrderedDict
12
  from fastapi import FastAPI, HTTPException, Request
13
  from fastapi.responses import StreamingResponse, JSONResponse, Response
 
217
  elif isinstance(item, dict):
218
  if item.get("type") == "text":
219
  parts.append(item.get("text", ""))
220
+ elif item.get("type") == "tool_result":
221
+ # استخراج نتيجة الأداة وتمريرها كنص
222
+ tool_content = item.get("content", "")
223
+ if isinstance(tool_content, list):
224
+ for tc in tool_content:
225
+ if isinstance(tc, dict) and tc.get("type") == "text":
226
+ parts.append(tc.get("text", ""))
227
+ elif isinstance(tc, str):
228
+ parts.append(tc)
229
+ elif isinstance(tool_content, str):
230
+ parts.append(tool_content)
231
+ # إضافة معلومات عن الأداة
232
+ tool_use_id = item.get("tool_use_id", "")
233
+ if tool_use_id:
234
+ parts.insert(len(parts) - 1, f"[Tool Result for {tool_use_id}]: ")
235
+ elif item.get("type") == "tool_use":
236
+ # تحويل طلب الأداة إلى نص مفهوم للنموذج
237
+ tool_name = item.get("name", "unknown")
238
+ tool_input = item.get("input", {})
239
+ parts.append(f"[Called tool: {tool_name} with input: {json.dumps(tool_input, ensure_ascii=False)}]")
240
  elif "text" in item:
241
  parts.append(item["text"])
242
  elif "content" in item:
 
246
  return str(content)
247
  return ""
248
 
249
+ # =====================================================
250
+ # TOOL CALL PARSER - محلل طلبات الأدوات من النص الخام
251
+ # يكتشف ويحلل وسوم <tool_call> من استجابة النموذج
252
+ # =====================================================
253
+ class ToolCallParser:
254
+ """
255
+ يكتشف أنماط Tool Call المختلفة من النماذج المفتوحة:
256
+ 1. <tool_call>{"name": "...", "arguments": {...}}</tool_call>
257
+ 2. ```tool_call\n{"name": "...", "arguments": {...}}\n```
258
+ 3. {"tool_calls": [{"function": {"name": "...", "arguments": {...}}}]}
259
+ 4. <function=name>{"arg": "val"}</function>
260
+ 5. [TOOL_CALL] name(args) [/TOOL_CALL]
261
+ """
262
+
263
+ # أنماط regex للكشف عن tool calls
264
+ PATTERNS = [
265
+ # النمط 1: <tool_call>JSON</tool_call>
266
+ re.compile(
267
+ r'<tool_call>\s*(\{.*?\})\s*</tool_call>',
268
+ re.DOTALL
269
+ ),
270
+ # النمط 2: ```tool_call\nJSON\n```
271
+ re.compile(
272
+ r'```(?:tool_call|json)?\s*\n?\s*(\{[^`]*?"(?:name|function)"[^`]*?\})\s*\n?```',
273
+ re.DOTALL
274
+ ),
275
+ # النمط 3: <function=name>JSON</function>
276
+ re.compile(
277
+ r'<function=(\w+)>\s*(\{.*?\})\s*</function>',
278
+ re.DOTALL
279
+ ),
280
+ # النمط 4: [TOOL_CALL]...[/TOOL_CALL]
281
+ re.compile(
282
+ r'\[TOOL_CALL\]\s*(.*?)\s*\[/TOOL_CALL\]',
283
+ re.DOTALL
284
+ ),
285
+ # النمط 5: ✿FUNCTION✿: name\n✿ARGS✿: JSON\n✿RESULT✿
286
+ re.compile(
287
+ r'✿FUNCTION✿:\s*(\w+)\s*\n✿ARGS✿:\s*(\{.*?\})\s*(?:\n✿RESULT✿)?',
288
+ re.DOTALL
289
+ ),
290
+ ]
291
+
292
+ # أنماط بداية tool call (للكشف المبكر أثناء الـ streaming)
293
+ START_MARKERS = [
294
+ '<tool_call>',
295
+ '```tool_call',
296
+ '<function=',
297
+ '[TOOL_CALL]',
298
+ '✿FUNCTION✿',
299
+ '{"tool_calls"',
300
+ '"tool_calls":',
301
+ ]
302
+
303
+ @staticmethod
304
+ def generate_tool_id() -> str:
305
+ """توليد معرف فريد للأداة بصيغة Anthropic"""
306
+ return f"toolu_{uuid.uuid4().hex[:24]}"
307
+
308
+ @classmethod
309
+ def might_contain_tool_call(cls, text: str) -> bool:
310
+ """فحص سريع: هل النص قد يحتوي على بداية tool call؟"""
311
+ text_lower = text.lower()
312
+ for marker in cls.START_MARKERS:
313
+ if marker.lower() in text_lower:
314
+ return True
315
+ return False
316
+
317
+ @classmethod
318
+ def parse_tool_calls(cls, text: str, available_tools: Optional[List[Dict]] = None) -> Tuple[str, List[Dict]]:
319
+ """
320
+ تحليل النص واستخراج tool calls منه.
321
+
322
+ يُرجع:
323
+ - النص النظيف (بدون وسوم الأدوات)
324
+ - قائمة بـ tool calls المكتشفة بصيغة Anthropic
325
+ """
326
+ tool_calls = []
327
+ clean_text = text
328
+
329
+ # النمط 1: <tool_call>JSON</tool_call>
330
+ pattern1_matches = re.finditer(
331
+ r'<tool_call>\s*(\{.*?\})\s*</tool_call>',
332
+ text, re.DOTALL
333
+ )
334
+ for match in pattern1_matches:
335
+ try:
336
+ raw_json = match.group(1).strip()
337
+ parsed = json.loads(raw_json)
338
+ tool_call = cls._normalize_tool_call(parsed, available_tools)
339
+ if tool_call:
340
+ tool_calls.append(tool_call)
341
+ clean_text = clean_text.replace(match.group(0), "")
342
+ except json.JSONDecodeError as e:
343
+ logger.warning(f"Failed to parse tool_call JSON: {e}")
344
+ # محاولة إصلاح JSON المكسور
345
+ fixed = cls._try_fix_json(raw_json)
346
+ if fixed:
347
+ tool_call = cls._normalize_tool_call(fixed, available_tools)
348
+ if tool_call:
349
+ tool_calls.append(tool_call)
350
+ clean_text = clean_text.replace(match.group(0), "")
351
+
352
+ # النمط 2: ```tool_call\nJSON\n```
353
+ if not tool_calls:
354
+ pattern2_matches = re.finditer(
355
+ r'```(?:tool_call|json)?\s*\n?\s*(\{[^`]*?"(?:name|function)"[^`]*?\})\s*\n?```',
356
+ text, re.DOTALL
357
+ )
358
+ for match in pattern2_matches:
359
+ try:
360
+ raw_json = match.group(1).strip()
361
+ parsed = json.loads(raw_json)
362
+ tool_call = cls._normalize_tool_call(parsed, available_tools)
363
+ if tool_call:
364
+ tool_calls.append(tool_call)
365
+ clean_text = clean_text.replace(match.group(0), "")
366
+ except json.JSONDecodeError:
367
+ pass
368
+
369
+ # النمط 3: <function=name>JSON</function>
370
+ if not tool_calls:
371
+ pattern3_matches = re.finditer(
372
+ r'<function=(\w+)>\s*(\{.*?\})\s*</function>',
373
+ text, re.DOTALL
374
+ )
375
+ for match in pattern3_matches:
376
+ try:
377
+ func_name = match.group(1)
378
+ args_json = json.loads(match.group(2).strip())
379
+ tool_call = {
380
+ "type": "tool_use",
381
+ "id": cls.generate_tool_id(),
382
+ "name": func_name,
383
+ "input": args_json
384
+ }
385
+ if available_tools:
386
+ tool_call = cls._validate_against_tools(tool_call, available_tools)
387
+ if tool_call:
388
+ tool_calls.append(tool_call)
389
+ clean_text = clean_text.replace(match.group(0), "")
390
+ except json.JSONDecodeError:
391
+ pass
392
+
393
+ # النمط 4: [TOOL_CALL]...[/TOOL_CALL]
394
+ if not tool_calls:
395
+ pattern4_matches = re.finditer(
396
+ r'\[TOOL_CALL\]\s*(.*?)\s*\[/TOOL_CALL\]',
397
+ text, re.DOTALL
398
+ )
399
+ for match in pattern4_matches:
400
+ content = match.group(1).strip()
401
+ try:
402
+ parsed = json.loads(content)
403
+ tool_call = cls._normalize_tool_call(parsed, available_tools)
404
+ if tool_call:
405
+ tool_calls.append(tool_call)
406
+ clean_text = clean_text.replace(match.group(0), "")
407
+ except json.JSONDecodeError:
408
+ # محاولة تحليل كـ function_name(args)
409
+ func_match = re.match(r'(\w+)\s*\((.*)\)', content, re.DOTALL)
410
+ if func_match:
411
+ try:
412
+ func_name = func_match.group(1)
413
+ args_str = func_match.group(2).strip()
414
+ args = json.loads(args_str) if args_str else {}
415
+ tool_call = {
416
+ "type": "tool_use",
417
+ "id": cls.generate_tool_id(),
418
+ "name": func_name,
419
+ "input": args
420
+ }
421
+ tool_calls.append(tool_call)
422
+ clean_text = clean_text.replace(match.group(0), "")
423
+ except json.JSONDecodeError:
424
+ pass
425
+
426
+ # النمط 5: ✿FUNCTION✿
427
+ if not tool_calls:
428
+ pattern5_matches = re.finditer(
429
+ r'✿FUNCTION✿:\s*(\w+)\s*\n✿ARGS✿:\s*(\{.*?\})\s*(?:\n✿RESULT✿)?',
430
+ text, re.DOTALL
431
+ )
432
+ for match in pattern5_matches:
433
+ try:
434
+ func_name = match.group(1)
435
+ args_json = json.loads(match.group(2).strip())
436
+ tool_call = {
437
+ "type": "tool_use",
438
+ "id": cls.generate_tool_id(),
439
+ "name": func_name,
440
+ "input": args_json
441
+ }
442
+ tool_calls.append(tool_call)
443
+ clean_text = clean_text.replace(match.group(0), "")
444
+ except json.JSONDecodeError:
445
+ pass
446
+
447
+ # النمط 6: JSON مباشر يحتوي tool_calls
448
+ if not tool_calls:
449
+ try:
450
+ # البحث عن JSON كامل في النص
451
+ json_matches = re.finditer(r'\{[^{}]*"tool_calls"[^{}]*\[.*?\]\s*\}', text, re.DOTALL)
452
+ for jm in json_matches:
453
+ try:
454
+ parsed = json.loads(jm.group(0))
455
+ if "tool_calls" in parsed:
456
+ for tc in parsed["tool_calls"]:
457
+ func_data = tc.get("function", tc)
458
+ name = func_data.get("name", "")
459
+ args = func_data.get("arguments", func_data.get("input", {}))
460
+ if isinstance(args, str):
461
+ args = json.loads(args)
462
+ if name:
463
+ tool_call = {
464
+ "type": "tool_use",
465
+ "id": cls.generate_tool_id(),
466
+ "name": name,
467
+ "input": args
468
+ }
469
+ tool_calls.append(tool_call)
470
+ clean_text = clean_text.replace(jm.group(0), "")
471
+ except json.JSONDecodeError:
472
+ pass
473
+ except Exception:
474
+ pass
475
+
476
+ # تنظيف النص من المسافات الزائدة
477
+ clean_text = clean_text.strip()
478
+ # إزالة أسطر فارغة متتالية
479
+ clean_text = re.sub(r'\n{3,}', '\n\n', clean_text)
480
+
481
+ return clean_text, tool_calls
482
+
483
+ @classmethod
484
+ def _normalize_tool_call(cls, parsed: Dict, available_tools: Optional[List[Dict]] = None) -> Optional[Dict]:
485
+ """
486
+ توحيد صيغة tool call من أشكال مختلفة إلى صيغة Anthropic.
487
+ يدعم:
488
+ - {"name": "x", "arguments": {...}}
489
+ - {"name": "x", "parameters": {...}}
490
+ - {"name": "x", "input": {...}}
491
+ - {"function": {"name": "x", "arguments": {...}}}
492
+ - {"tool": "x", "args": {...}}
493
+ """
494
+ name = None
495
+ arguments = {}
496
+
497
+ # استخراج الاسم
498
+ if "function" in parsed and isinstance(parsed["function"], dict):
499
+ name = parsed["function"].get("name")
500
+ arguments = parsed["function"].get("arguments", {})
501
+ elif "name" in parsed:
502
+ name = parsed["name"]
503
+ arguments = (
504
+ parsed.get("arguments") or
505
+ parsed.get("parameters") or
506
+ parsed.get("input") or
507
+ parsed.get("args") or
508
+ {}
509
+ )
510
+ elif "tool" in parsed:
511
+ name = parsed["tool"]
512
+ arguments = parsed.get("args", parsed.get("arguments", parsed.get("input", {})))
513
+
514
+ if not name:
515
+ return None
516
+
517
+ # تحويل arguments من string إلى dict إذا لزم
518
+ if isinstance(arguments, str):
519
+ try:
520
+ arguments = json.loads(arguments)
521
+ except json.JSONDecodeError:
522
+ arguments = {"raw_input": arguments}
523
+
524
+ tool_call = {
525
+ "type": "tool_use",
526
+ "id": cls.generate_tool_id(),
527
+ "name": str(name),
528
+ "input": arguments if isinstance(arguments, dict) else {"value": arguments}
529
+ }
530
+
531
+ # التحقق من صحة الأداة مقابل القائمة المتاحة
532
+ if available_tools:
533
+ tool_call = cls._validate_against_tools(tool_call, available_tools)
534
+
535
+ return tool_call
536
+
537
+ @classmethod
538
+ def _validate_against_tools(cls, tool_call: Dict, available_tools: List[Dict]) -> Optional[Dict]:
539
+ """
540
+ التحقق من أن الأداة المطلوبة موجودة في قائمة الأدوات المتاحة.
541
+ إذا لم تكن موجودة بالضبط، يحاول إيجاد أقرب تطابق.
542
+ """
543
+ requested_name = tool_call["name"]
544
+ tool_names = []
545
+
546
+ for tool in available_tools:
547
+ tool_name = tool.get("name", "")
548
+ if not tool_name and "function" in tool:
549
+ tool_name = tool["function"].get("name", "")
550
+ tool_names.append(tool_name)
551
+
552
+ if tool_name == requested_name:
553
+ # تطابق مباشر - التحقق من المعاملات
554
+ return tool_call
555
+
556
+ # محاولة تطابق جزئي (fuzzy matching)
557
+ requested_lower = requested_name.lower().replace("_", "").replace("-", "")
558
+ for tool_name in tool_names:
559
+ tool_lower = tool_name.lower().replace("_", "").replace("-", "")
560
+ if requested_lower == tool_lower:
561
+ tool_call["name"] = tool_name # استخدام الاسم الصحيح
562
+ return tool_call
563
+ # تطابق يحتوي
564
+ if requested_lower in tool_lower or tool_lower in requested_lower:
565
+ tool_call["name"] = tool_name
566
+ return tool_call
567
+
568
+ # لم يتم العثور على تطابق - إرسالها كما هي (قد تكون أداة MCP)
569
+ logger.warning(f"Tool '{requested_name}' not found in available tools: {tool_names}")
570
+ return tool_call
571
+
572
+ @classmethod
573
+ def _try_fix_json(cls, broken_json: str) -> Optional[Dict]:
574
+ """محاولة إصلاح JSON مكسور"""
575
+ # إزالة trailing commas
576
+ fixed = re.sub(r',\s*([}\]])', r'\1', broken_json)
577
+ # إضافة أقواس مفقودة
578
+ open_braces = fixed.count('{') - fixed.count('}')
579
+ if open_braces > 0:
580
+ fixed += '}' * open_braces
581
+ open_brackets = fixed.count('[') - fixed.count(']')
582
+ if open_brackets > 0:
583
+ fixed += ']' * open_brackets
584
+
585
+ try:
586
+ return json.loads(fixed)
587
+ except json.JSONDecodeError:
588
+ pass
589
+
590
+ # محاولة أخرى: استخراج أول JSON صالح
591
+ try:
592
+ # البحث عن بداية ونهاية JSON
593
+ start = fixed.index('{')
594
+ depth = 0
595
+ for i in range(start, len(fixed)):
596
+ if fixed[i] == '{':
597
+ depth += 1
598
+ elif fixed[i] == '}':
599
+ depth -= 1
600
+ if depth == 0:
601
+ return json.loads(fixed[start:i+1])
602
+ except (ValueError, json.JSONDecodeError):
603
+ pass
604
+
605
+ return None
606
+
607
+
608
+ # =====================================================
609
+ # TOOL CALL STREAM BUFFER - لاكتشاف tool calls أثناء الـ streaming
610
+ # =====================================================
611
+ class StreamToolBuffer:
612
+ """
613
+ Buffer ذكي يجمع chunks الـ stream ويكتشف tool calls.
614
+ يعمل بنمطين:
615
+ 1. نمط النص العادي: يمرر النص مباشرة
616
+ 2. نمط Tool Call: يجمع النص حتى يكتمل الوسم ثم يحلله
617
+ """
618
+
619
+ def __init__(self, available_tools: Optional[List[Dict]] = None):
620
+ self.buffer = ""
621
+ self.in_tool_call = False
622
+ self.tool_call_buffer = ""
623
+ self.available_tools = available_tools or []
624
+ self.pending_text = ""
625
+ self.tool_call_depth = 0
626
+
627
+ def feed(self, chunk: str) -> List[Dict]:
628
+ """
629
+ إطعام chunk جديد للـ buffer.
630
+ يُرجع قائمة من الأحداث (events) الجاهزة للإرسال.
631
+
632
+ كل حدث يكون إما:
633
+ - {"type": "text", "text": "..."}
634
+ - {"type": "tool_use", "id": "...", "name": "...", "input": {...}}
635
+ """
636
+ events = []
637
+ self.buffer += chunk
638
+
639
+ while self.buffer:
640
+ if self.in_tool_call:
641
+ # نحن داخل وسم tool_call، نجمع حتى نجد الإغلاق
642
+ events.extend(self._process_tool_call_mode())
643
+ if self.in_tool_call:
644
+ break # لم يكتمل الوسم بعد، ننتظر المزيد
645
+ else:
646
+ # نمط عادي: نبحث عن بداية tool call
647
+ events.extend(self._process_text_mode())
648
+ if self.in_tool_call:
649
+ continue # وجدنا بداية tool call، نكمل المعالجة
650
+ break # لا يوجد tool call، ننتظر المزيد
651
+
652
+ return events
653
+
654
+ def _process_text_mode(self) -> List[Dict]:
655
+ """معالجة النص في النمط العادي"""
656
+ events = []
657
+
658
+ # البحث عن أي من علامات بداية tool call
659
+ earliest_pos = -1
660
+ earliest_marker = ""
661
+ for marker in ToolCallParser.START_MARKERS:
662
+ pos = self.buffer.lower().find(marker.lower())
663
+ if pos != -1 and (earliest_pos == -1 or pos < earliest_pos):
664
+ earliest_pos = pos
665
+ earliest_marker = marker
666
+
667
+ if earliest_pos == -1:
668
+ # لا يوجد tool call - لكن نحتفظ بآخر 50 حرف كاحتياط
669
+ # (قد يكون بداية وسم مقطوع)
670
+ safe_length = len(self.buffer) - 50
671
+ if safe_length > 0:
672
+ text_to_send = self.buffer[:safe_length]
673
+ self.buffer = self.buffer[safe_length:]
674
+ if text_to_send:
675
+ events.append({"type": "text", "text": text_to_send})
676
+ # إذا كان الـ buffer صغير جداً، لا نرسل شيء حتى يكبر
677
+ else:
678
+ # وجدنا بداية tool call
679
+ if earliest_pos > 0:
680
+ # إرسال النص قبل الـ tool call
681
+ text_before = self.buffer[:earliest_pos]
682
+ if text_before.strip():
683
+ events.append({"type": "text", "text": text_before})
684
+
685
+ # الانتقال لنمط tool call
686
+ self.buffer = self.buffer[earliest_pos:]
687
+ self.in_tool_call = True
688
+ self.tool_call_buffer = ""
689
+
690
+ return events
691
+
692
+ def _process_tool_call_mode(self) -> List[Dict]:
693
+ """معالجة النص في نمط tool call"""
694
+ events = []
695
+
696
+ # البحث عن نهاية الوسم
697
+ end_markers = [
698
+ ('</tool_call>', '<tool_call>'),
699
+ ('```', '```tool_call'),
700
+ ('```', '```json'),
701
+ ('</function>', '<function='),
702
+ ('[/TOOL_CALL]', '[TOOL_CALL]'),
703
+ ]
704
+
705
+ found_end = False
706
+ for end_marker, start_marker in end_markers:
707
+ # تخطي علامة البداية والبحث عن النهاية
708
+ search_start = len(start_marker) if self.buffer.lower().startswith(start_marker.lower()) else 0
709
+ end_pos = self.buffer.lower().find(end_marker.lower(), search_start)
710
+
711
+ if end_pos != -1:
712
+ # وجدنا النهاية
713
+ full_tool_text = self.buffer[:end_pos + len(end_marker)]
714
+ remaining = self.buffer[end_pos + len(end_marker):]
715
+
716
+ # تحليل الـ tool call
717
+ clean_text, tool_calls = ToolCallParser.parse_tool_calls(
718
+ full_tool_text, self.available_tools
719
+ )
720
+
721
+ for tc in tool_calls:
722
+ events.append(tc)
723
+
724
+ # إذا بقي نص نظيف
725
+ if clean_text.strip():
726
+ events.append({"type": "text", "text": clean_text})
727
+
728
+ self.buffer = remaining
729
+ self.in_tool_call = False
730
+ found_end = True
731
+ break
732
+
733
+ if not found_end:
734
+ # لم نجد نهاية بعد - نتحقق من الحجم
735
+ if len(self.buffer) > 5000:
736
+ # Buffer كبير جداً بدون إغلاق - على الأرجح ليس tool call حقيقي
737
+ logger.warning("Tool call buffer too large without closing tag, treating as text")
738
+ events.append({"type": "text", "text": self.buffer})
739
+ self.buffer = ""
740
+ self.in_tool_call = False
741
+
742
+ return events
743
+
744
+ def flush(self) -> List[Dict]:
745
+ """
746
+ تفريغ أي محتوى متبقي في الـ buffer.
747
+ يُستدعى عند انتهاء الـ stream.
748
+ """
749
+ events = []
750
+
751
+ if self.in_tool_call and self.buffer:
752
+ # محاولة تحليل ما تبقى كـ tool call
753
+ clean_text, tool_calls = ToolCallParser.parse_tool_calls(
754
+ self.buffer, self.available_tools
755
+ )
756
+ for tc in tool_calls:
757
+ events.append(tc)
758
+ if clean_text.strip():
759
+ events.append({"type": "text", "text": clean_text})
760
+ elif self.buffer:
761
+ # نص عادي متبقي
762
+ if self.buffer.strip():
763
+ events.append({"type": "text", "text": self.buffer})
764
+
765
+ self.buffer = ""
766
+ self.in_tool_call = False
767
+ return events
768
+
769
+
770
+ # =====================================================
771
+ # TOOLS FORMATTER - تحويل أدوات Anthropic إلى نص للنموذج
772
+ # =====================================================
773
+ class ToolsFormatter:
774
+ """
775
+ يأخذ مصفوفة tools من طلب Anthropic ويحولها إلى تعليمات نصية
776
+ تُضاف إلى System Prompt لتوجيه النموذج لاستخدام الأدوات.
777
+ """
778
+
779
+ @staticmethod
780
+ def format_tools_for_prompt(tools: List[Dict], tool_choice: Any = None) -> str:
781
+ """
782
+ تحويل تعريفات الأدوات إلى نص يُضاف للـ system prompt.
783
+ """
784
+ if not tools:
785
+ return ""
786
+
787
+ lines = []
788
+ lines.append("# Available Tools")
789
+ lines.append("")
790
+ lines.append("You have access to the following tools. To use a tool, respond with a tool_call block.")
791
+ lines.append("IMPORTANT: When you need to use a tool, you MUST format your response EXACTLY like this:")
792
+ lines.append("")
793
+ lines.append("<tool_call>")
794
+ lines.append('{"name": "tool_name", "arguments": {"param1": "value1", "param2": "value2"}}')
795
+ lines.append("</tool_call>")
796
+ lines.append("")
797
+ lines.append("You can include text explanation before and/or after the tool_call block.")
798
+ lines.append("You can make multiple tool calls in a single response by using multiple <tool_call> blocks.")
799
+ lines.append("")
800
+ lines.append("## Tool Definitions:")
801
+ lines.append("")
802
+
803
+ for i, tool in enumerate(tools, 1):
804
+ name = tool.get("name", "unknown")
805
+ description = tool.get("description", "No description")
806
+ input_schema = tool.get("input_schema", {})
807
+
808
+ lines.append(f"### {i}. `{name}`")
809
+ lines.append(f"**Description:** {description}")
810
+
811
+ # معالجة المعاملات
812
+ properties = input_schema.get("properties", {})
813
+ required = input_schema.get("required", [])
814
+
815
+ if properties:
816
+ lines.append("**Parameters:**")
817
+ for param_name, param_info in properties.items():
818
+ param_type = param_info.get("type", "any")
819
+ param_desc = param_info.get("description", "")
820
+ is_required = param_name in required
821
+ req_marker = " (required)" if is_required else " (optional)"
822
+ lines.append(f" - `{param_name}` ({param_type}{req_marker}): {param_desc}")
823
+
824
+ # إضافة enum values إذا وجدت
825
+ if "enum" in param_info:
826
+ lines.append(f" Allowed values: {', '.join(str(v) for v in param_info['enum'])}")
827
+ else:
828
+ lines.append("**Parameters:** None")
829
+
830
+ lines.append("")
831
+
832
+ # إضافة تعليمات خاصة بـ tool_choice
833
+ if tool_choice:
834
+ if isinstance(tool_choice, dict):
835
+ if tool_choice.get("type") == "tool":
836
+ forced_tool = tool_choice.get("name", "")
837
+ if forced_tool:
838
+ lines.append(f"**IMPORTANT:** You MUST use the `{forced_tool}` tool in your response.")
839
+ elif tool_choice.get("type") == "any":
840
+ lines.append("**IMPORTANT:** You MUST use at least one tool in your response.")
841
+ elif tool_choice == "auto":
842
+ lines.append("Use tools when appropriate to accomplish the task.")
843
+ elif tool_choice == "any":
844
+ lines.append("**IMPORTANT:** You MUST use at least one tool in your response.")
845
+
846
+ lines.append("")
847
+ lines.append("Remember: Always use <tool_call>JSON</tool_call> format when calling tools.")
848
+ lines.append("The JSON inside must have 'name' and 'arguments' keys.")
849
+ lines.append("")
850
+
851
+ return "\n".join(lines)
852
+
853
+ @staticmethod
854
+ def format_tool_result_for_message(tool_use_id: str, content: Any) -> str:
855
+ """تحويل نتيجة أداة إلى نص مفهوم للنموذج"""
856
+ result_text = ""
857
+ if isinstance(content, str):
858
+ result_text = content
859
+ elif isinstance(content, list):
860
+ parts = []
861
+ for item in content:
862
+ if isinstance(item, dict):
863
+ if item.get("type") == "text":
864
+ parts.append(item.get("text", ""))
865
+ elif item.get("type") == "image":
866
+ parts.append("[Image content]")
867
+ else:
868
+ parts.append(json.dumps(item, ensure_ascii=False))
869
+ elif isinstance(item, str):
870
+ parts.append(item)
871
+ result_text = "\n".join(parts)
872
+ elif isinstance(content, dict):
873
+ result_text = json.dumps(content, ensure_ascii=False, indent=2)
874
+ else:
875
+ result_text = str(content) if content else ""
876
+
877
+ return f"[Tool Result (id: {tool_use_id})]\n{result_text}\n[/Tool Result]"
878
+
879
+
880
+ # =====================================================
881
+ # MESSAGE CONVERTER - تحويل رسائل Anthropic إلى صيغة g4f
882
+ # =====================================================
883
+ class MessageConverter:
884
+ """
885
+ يحول رسائل Anthropic (التي قد تحتوي tool_use و tool_result)
886
+ إلى رسائل نصية بسيطة يفهمها g4f.
887
+ """
888
+
889
+ @staticmethod
890
+ def convert_messages(messages: List[Dict], system_prompt: str = "",
891
+ tools: Optional[List[Dict]] = None,
892
+ tool_choice: Any = None) -> Tuple[str, List[Dict]]:
893
+ """
894
+ تحويل رسائل Anthropic إلى (full_message, history) لـ g4f.
895
+
896
+ يُرجع:
897
+ - full_message: الرسالة الأخيرة مع system prompt
898
+ - history: سجل المحادثة
899
+ """
900
+ history = []
901
+
902
+ # بناء system prompt مع تعريفات الأدوات
903
+ full_system = ""
904
+ if system_prompt:
905
+ full_system = system_prompt
906
+
907
+ if tools:
908
+ tools_text = ToolsFormatter.format_tools_for_prompt(tools, tool_choice)
909
+ if full_system:
910
+ full_system = f"{full_system}\n\n{tools_text}"
911
+ else:
912
+ full_system = tools_text
913
+
914
+ # تحويل كل رسالة
915
+ for msg in messages:
916
+ role = msg.get("role", "user")
917
+ content = msg.get("content", "")
918
+
919
+ if role == "system":
920
+ # إضافة system message إلى system prompt
921
+ sys_text = extract_text_from_content(content)
922
+ if full_system:
923
+ full_system = f"{full_system}\n\n{sys_text}"
924
+ else:
925
+ full_system = sys_text
926
+ continue
927
+
928
+ # تحويل content المعقد
929
+ converted_text = MessageConverter._convert_content(content, role)
930
+
931
+ if converted_text:
932
+ # تحويل role
933
+ g4f_role = "user" if role == "user" else "assistant"
934
+ history.append({"role": g4f_role, "content": converted_text})
935
+
936
+ # استخراج آخر رسالة كـ user message
937
+ if history:
938
+ last_msg = history.pop()
939
+ user_message = last_msg["content"]
940
+ else:
941
+ user_message = ""
942
+
943
+ # إضافة system prompt
944
+ if full_system:
945
+ full_message = f"[System Instructions]\n{full_system}\n[/System Instructions]\n\n{user_message}"
946
+ else:
947
+ full_message = user_message
948
+
949
+ return full_message, history
950
+
951
+ @staticmethod
952
+ def _convert_content(content: Any, role: str) -> str:
953
+ """تحويل محتوى رسالة واحدة"""
954
+ if isinstance(content, str):
955
+ return content
956
+
957
+ if isinstance(content, list):
958
+ parts = []
959
+ for block in content:
960
+ if isinstance(block, str):
961
+ parts.append(block)
962
+ elif isinstance(block, dict):
963
+ block_type = block.get("type", "")
964
+
965
+ if block_type == "text":
966
+ parts.append(block.get("text", ""))
967
+
968
+ elif block_type == "tool_use":
969
+ # تحويل طلب أداة سابق إلى نص
970
+ name = block.get("name", "unknown")
971
+ input_data = block.get("input", {})
972
+ tool_id = block.get("id", "")
973
+ parts.append(
974
+ f"<tool_call>"
975
+ f'{{"name": "{name}", "arguments": {json.dumps(input_data, ensure_ascii=False)}}}'
976
+ f"</tool_call>"
977
+ )
978
+
979
+ elif block_type == "tool_result":
980
+ # تحويل نتيجة أداة
981
+ tool_use_id = block.get("tool_use_id", "")
982
+ result_content = block.get("content", "")
983
+ is_error = block.get("is_error", False)
984
+
985
+ result_text = ToolsFormatter.format_tool_result_for_message(
986
+ tool_use_id, result_content
987
+ )
988
+ if is_error:
989
+ result_text = f"[ERROR] {result_text}"
990
+ parts.append(result_text)
991
+
992
+ elif block_type == "image":
993
+ parts.append("[Image content - not supported in text mode]")
994
+
995
+ else:
996
+ # نوع غير معروف
997
+ if "text" in block:
998
+ parts.append(block["text"])
999
+ else:
1000
+ parts.append(json.dumps(block, ensure_ascii=False))
1001
+
1002
+ return "\n".join(parts)
1003
+
1004
+ if content is not None:
1005
+ return str(content)
1006
+ return ""
1007
+
1008
+
1009
  # =====================================================
1010
  # CHAT LOGIC - مع fallback ذكي
1011
  # =====================================================
 
1016
  return
1017
 
1018
  # مفتاح التخزين المؤقت
1019
+ key = f"{provider_name}|{model_name}|{message[:200]}"
1020
  cached = CACHE.get(key)
1021
  if cached:
1022
  yield cached
 
1026
  msgs = []
1027
  try:
1028
  if history:
1029
+ if isinstance(history, list) and len(history) > 0:
1030
+ if isinstance(history[0], dict):
1031
+ for item in history[-40:]:
1032
+ role = item.get("role")
1033
+ content = item.get("content")
1034
+ if role and content:
1035
+ text = extract_text_from_content(content) if not isinstance(content, str) else content
1036
+ if text:
1037
+ msgs.append({"role": str(role), "content": text})
1038
+ else:
1039
+ for item in history[-20:]:
1040
+ if isinstance(item, (list, tuple)) and len(item) == 2:
1041
+ if item[0]:
1042
+ msgs.append({"role": "user", "content": str(item[0])})
1043
+ if item[1]:
1044
+ msgs.append({"role": "assistant", "content": str(item[1])})
1045
  except Exception as e:
1046
  logger.warning(f"History error: {e}")
1047
 
 
1111
  # =====================================================
1112
  # FASTAPI
1113
  # =====================================================
1114
+ app = FastAPI(title="G4F Smart Router", description="AI Gateway - Protocol Translator for Anthropic")
1115
 
1116
  # إضافة CORS للسماح بالطلبات من أي مصدر
1117
  app.add_middleware(
 
1176
  return Response(status_code=200)
1177
 
1178
  # =====================================================
1179
+ # نقاط نهاية متوافقة مع Claude Cowork و Anthropic API
1180
  # =====================================================
1181
  @app.get("/v1/models")
1182
  async def v1_models(request: Request):
 
1207
  return {"object": "list", "data": models}
1208
 
1209
  # =====================================================
1210
+ # نقطة /v1/messages - بروتوكول Anthropic الكامل
1211
+ # مع دعم Tools و Multi-Block Content
1212
  # =====================================================
1213
  @app.post("/v1/messages")
1214
  async def v1_messages(request: Request):
1215
  verify_api_key(request)
1216
  body = await request.json()
1217
 
1218
+ logger.info(f"Received /v1/messages request: model={body.get('model')}, stream={body.get('stream')}, "
1219
+ f"tools_count={len(body.get('tools', []))}")
1220
+
1221
  messages = body.get("messages", [])
1222
  if not messages:
1223
  raise HTTPException(status_code=400, detail="No messages provided")
1224
 
1225
+ # استخراج المعاملات
1226
  model = body.get("model", "gpt-4o")
1227
  system_prompt = body.get("system", "")
1228
+ # دعم system كـ string أو list
1229
+ if isinstance(system_prompt, list):
1230
+ sys_parts = []
1231
+ for sp in system_prompt:
1232
+ if isinstance(sp, dict) and sp.get("type") == "text":
1233
+ sys_parts.append(sp.get("text", ""))
1234
+ elif isinstance(sp, str):
1235
+ sys_parts.append(sp)
1236
+ system_prompt = "\n".join(sys_parts)
1237
+
1238
  is_stream = body.get("stream", False)
1239
  max_tokens = body.get("max_tokens", 4096)
1240
+ tools = body.get("tools", [])
1241
+ tool_choice = body.get("tool_choice", "auto")
1242
+ stop_sequences = body.get("stop_sequences", [])
1243
+ temperature = body.get("temperature", 1.0)
1244
+ top_p = body.get("top_p", None)
1245
+ top_k = body.get("top_k", None)
1246
+ metadata = body.get("metadata", {})
1247
+
1248
+ # تحويل الرسائل باستخدام MessageConverter
1249
+ full_message, history = MessageConverter.convert_messages(
1250
+ messages, system_prompt, tools, tool_choice
1251
+ )
1252
 
1253
+ logger.info(f"Converted message length: {len(full_message)}, history length: {len(history)}, "
1254
+ f"tools: {[t.get('name', '') for t in tools]}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1255
 
1256
  # إذا كان stream مطلوب
1257
  if is_stream:
1258
+ return await _handle_anthropic_stream(
1259
+ full_message, history, model, max_tokens, tools, tool_choice, metadata
1260
+ )
1261
 
1262
+ # وضع non-stream: تجميع الرد كاملاً ثم تحليله
1263
  full_response = ""
1264
  for chunk in ask(full_message, history, "Blackbox", model):
1265
+ full_response += chunk
1266
+
1267
+ # تحليل الرد للبحث عن tool calls
1268
+ clean_text, tool_calls = ToolCallParser.parse_tool_calls(full_response, tools)
1269
 
1270
  message_id = f"msg_{int(time.time())}_{os.urandom(4).hex()}"
1271
+ input_tokens = max(1, len(full_message) // 4)
1272
  output_tokens = max(1, len(full_response) // 4)
1273
 
1274
+ # بناء content blocks
1275
+ content_blocks = []
1276
+
1277
+ # إضافة النص إذا وجد
1278
+ if clean_text.strip():
1279
+ content_blocks.append({
1280
+ "type": "text",
1281
+ "text": clean_text.strip()
1282
+ })
1283
+
1284
+ # إضافة tool calls إذا وجدت
1285
+ for tc in tool_calls:
1286
+ content_blocks.append({
1287
+ "type": "tool_use",
1288
+ "id": tc.get("id", ToolCallParser.generate_tool_id()),
1289
+ "name": tc["name"],
1290
+ "input": tc.get("input", {})
1291
+ })
1292
+
1293
+ # إذا لم يكن هناك أي محتوى
1294
+ if not content_blocks:
1295
+ content_blocks.append({
1296
+ "type": "text",
1297
+ "text": full_response or ""
1298
+ })
1299
+
1300
+ # تحديد stop_reason
1301
+ stop_reason = "end_turn"
1302
+ if tool_calls:
1303
+ stop_reason = "tool_use"
1304
+
1305
  return {
1306
  "id": message_id,
1307
  "type": "message",
1308
  "role": "assistant",
1309
+ "content": content_blocks,
1310
  "model": model,
1311
+ "stop_reason": stop_reason,
1312
  "stop_sequence": None,
1313
  "usage": {
1314
  "input_tokens": input_tokens,
 
1318
 
1319
  # =====================================================
1320
  # معالج الـ streaming بصيغة Anthropic SSE
1321
+ # مع دعم كامل لـ Tool Use و Multi-Block Content
1322
  # =====================================================
1323
+ async def _handle_anthropic_stream(
1324
+ full_message: str,
1325
+ history: list,
1326
+ model: str,
1327
+ max_tokens: int,
1328
+ tools: List[Dict] = None,
1329
+ tool_choice: Any = None,
1330
+ metadata: Dict = None
1331
+ ):
1332
  async def generate_stream():
1333
  message_id = f"msg_{int(time.time())}_{os.urandom(4).hex()}"
1334
+ tools_list = tools or []
1335
 
1336
  # حدث بداية الرسالة
1337
  msg_start = {
 
1352
  }
1353
  yield f"event: message_start\ndata: {json.dumps(msg_start, ensure_ascii=False)}\n\n"
1354
 
1355
+ # إنشاء StreamToolBuffer
1356
+ tool_buffer = StreamToolBuffer(available_tools=tools_list)
 
 
 
 
 
 
 
 
1357
 
1358
+ # متتبع الكتل (blocks)
1359
+ current_block_index = 0
1360
+ block_started = False
1361
+ text_block_open = False
1362
  output_tokens = 0
1363
+ has_tool_calls = False
1364
+ accumulated_text = ""
1365
+
1366
+ # دالة مساعدة لبدء كتلة نصية
1367
+ def make_text_block_start(index: int) -> str:
1368
+ block_start = {
1369
+ "type": "content_block_start",
1370
+ "index": index,
1371
+ "content_block": {
1372
+ "type": "text",
1373
+ "text": ""
1374
+ }
1375
+ }
1376
+ return f"event: content_block_start\ndata: {json.dumps(block_start, ensure_ascii=False)}\n\n"
1377
+
1378
+ # دالة مساعدة لإرسال text delta
1379
+ def make_text_delta(index: int, text: str) -> str:
1380
+ delta_event = {
1381
+ "type": "content_block_delta",
1382
+ "index": index,
1383
+ "delta": {
1384
+ "type": "text_delta",
1385
+ "text": text
1386
+ }
1387
+ }
1388
+ return f"event: content_block_delta\ndata: {json.dumps(delta_event, ensure_ascii=False)}\n\n"
1389
+
1390
+ # دالة مساعدة لإنهاء كتلة
1391
+ def make_block_stop(index: int) -> str:
1392
+ block_stop = {
1393
+ "type": "content_block_stop",
1394
+ "index": index
1395
+ }
1396
+ return f"event: content_block_stop\ndata: {json.dumps(block_stop, ensure_ascii=False)}\n\n"
1397
+
1398
+ # دالة مساعدة لإرسال tool_use block كامل
1399
+ def make_tool_use_events(index: int, tool_call: Dict) -> str:
1400
+ events_str = ""
1401
+ tool_id = tool_call.get("id", ToolCallParser.generate_tool_id())
1402
+ tool_name = tool_call.get("name", "unknown")
1403
+ tool_input = tool_call.get("input", {})
1404
+
1405
+ # بداية كتلة tool_use
1406
+ block_start = {
1407
+ "type": "content_block_start",
1408
+ "index": index,
1409
+ "content_block": {
1410
+ "type": "tool_use",
1411
+ "id": tool_id,
1412
+ "name": tool_name,
1413
+ "input": {}
1414
+ }
1415
+ }
1416
+ events_str += f"event: content_block_start\ndata: {json.dumps(block_start, ensure_ascii=False)}\n\n"
1417
+
1418
+ # إرسال input كـ JSON delta
1419
+ input_json = json.dumps(tool_input, ensure_ascii=False)
1420
+ # تقسيم JSON إلى أجزاء للـ streaming
1421
+ chunk_size = 100
1422
+ for i in range(0, len(input_json), chunk_size):
1423
+ json_chunk = input_json[i:i + chunk_size]
1424
  delta_event = {
1425
  "type": "content_block_delta",
1426
+ "index": index,
1427
  "delta": {
1428
+ "type": "input_json_delta",
1429
+ "partial_json": json_chunk
1430
  }
1431
  }
1432
+ events_str += f"event: content_block_delta\ndata: {json.dumps(delta_event, ensure_ascii=False)}\n\n"
1433
 
1434
+ # نهاية كتلة tool_use
1435
+ events_str += make_block_stop(index)
1436
+
1437
+ return events_str
1438
+
1439
+ # ===== بدء معالجة الـ Stream =====
1440
+ try:
1441
+ for chunk in ask(full_message, history, "Blackbox", model):
1442
+ if not chunk:
1443
+ continue
1444
+
1445
+ output_tokens += max(1, len(chunk) // 4)
1446
+
1447
+ # إذا لا توجد أدوات معرفة، نرسل كنص مباشرة (أسرع)
1448
+ if not tools_list:
1449
+ if not text_block_open:
1450
+ yield make_text_block_start(current_block_index)
1451
+ text_block_open = True
1452
+ yield make_text_delta(current_block_index, chunk)
1453
+ continue
1454
+
1455
+ # هناك أدوات - نستخدم الـ buffer للكشف عن tool calls
1456
+ events = tool_buffer.feed(chunk)
1457
+
1458
+ for event in events:
1459
+ if event.get("type") == "text":
1460
+ text = event["text"]
1461
+ if text:
1462
+ if not text_block_open:
1463
+ yield make_text_block_start(current_block_index)
1464
+ text_block_open = True
1465
+ yield make_text_delta(current_block_index, text)
1466
+
1467
+ elif event.get("type") == "tool_use":
1468
+ # إغلاق كتلة النص المفتوحة أولاً
1469
+ if text_block_open:
1470
+ yield make_block_stop(current_block_index)
1471
+ current_block_index += 1
1472
+ text_block_open = False
1473
+
1474
+ # إرسال كتلة tool_use
1475
+ has_tool_calls = True
1476
+ yield make_tool_use_events(current_block_index, event)
1477
+ current_block_index += 1
1478
+
1479
+ # ===== تفريغ الـ buffer المتبقي =====
1480
+ remaining_events = tool_buffer.flush()
1481
+ for event in remaining_events:
1482
+ if event.get("type") == "text":
1483
+ text = event["text"]
1484
+ if text:
1485
+ if not text_block_open:
1486
+ yield make_text_block_start(current_block_index)
1487
+ text_block_open = True
1488
+ yield make_text_delta(current_block_index, text)
1489
+
1490
+ elif event.get("type") == "tool_use":
1491
+ if text_block_open:
1492
+ yield make_block_stop(current_block_index)
1493
+ current_block_index += 1
1494
+ text_block_open = False
1495
+
1496
+ has_tool_calls = True
1497
+ yield make_tool_use_events(current_block_index, event)
1498
+ current_block_index += 1
1499
+
1500
+ except Exception as e:
1501
+ logger.error(f"Stream error: {str(e)}")
1502
+ # إرسال رسالة خطأ كنص
1503
+ if not text_block_open:
1504
+ yield make_text_block_start(current_block_index)
1505
+ text_block_open = True
1506
+ yield make_text_delta(current_block_index, f"\n\n[Error: {str(e)}]")
1507
+
1508
+ # إغلاق آخر كتلة مفتوحة
1509
+ if text_block_open:
1510
+ yield make_block_stop(current_block_index)
1511
+ elif current_block_index == 0 and not has_tool_calls:
1512
+ # لم يتم إرسال أي كتلة - نرسل كتلة نصية فارغة
1513
+ yield make_text_block_start(0)
1514
+ yield make_text_delta(0, "")
1515
+ yield make_block_stop(0)
1516
+
1517
+ # تحديد stop_reason
1518
+ stop_reason = "tool_use" if has_tool_calls else "end_turn"
1519
 
1520
  # حدث دلتا الرسالة (نهاية)
1521
  msg_delta = {
1522
  "type": "message_delta",
1523
  "delta": {
1524
+ "stop_reason": stop_reason,
1525
  "stop_sequence": None
1526
  },
1527
  "usage": {
 
1544
  )
1545
 
1546
  # =====================================================
1547
+ # نقطة /v1/messages/stream منفصلة (للتوافق)
1548
  # =====================================================
1549
  @app.post("/v1/messages/stream")
1550
  async def v1_messages_stream(request: Request):
 
1555
  if not messages:
1556
  raise HTTPException(status_code=400, detail="No messages provided")
1557
 
 
 
1558
  model = body.get("model", "gpt-4o")
1559
  system_prompt = body.get("system", "")
1560
+ if isinstance(system_prompt, list):
1561
+ sys_parts = []
1562
+ for sp in system_prompt:
1563
+ if isinstance(sp, dict) and sp.get("type") == "text":
1564
+ sys_parts.append(sp.get("text", ""))
1565
+ elif isinstance(sp, str):
1566
+ sys_parts.append(sp)
1567
+ system_prompt = "\n".join(sys_parts)
1568
 
1569
+ max_tokens = body.get("max_tokens", 4096)
1570
+ tools = body.get("tools", [])
1571
+ tool_choice = body.get("tool_choice", "auto")
 
 
 
 
1572
 
1573
+ # تحويل الرسائل
1574
+ full_message, history = MessageConverter.convert_messages(
1575
+ messages, system_prompt, tools, tool_choice
1576
+ )
1577
 
1578
+ return await _handle_anthropic_stream(full_message, history, model, max_tokens, tools, tool_choice)
1579
 
1580
  # =====================================================
1581
+ # نقطة /v1/chat/completions (صيغة OpenAI)
 
1582
  # =====================================================
1583
  @app.post("/v1/chat/completions")
1584
  async def v1_chat_completions(request: Request):
 
1680
  @app.get("/")
1681
  async def root():
1682
  return {
1683
+ "message": "G4F Smart Router - Protocol Translator (Anthropic Compatible)",
1684
+ "version": "2.0.0",
1685
+ "features": [
1686
+ "Tool Call Detection & Translation",
1687
+ "Multi-Block Content Support",
1688
+ "Anthropic Protocol Full Compliance",
1689
+ "Tool Definitions Forwarding",
1690
+ "Stream & Non-Stream Support"
1691
+ ],
1692
  "providers": list(REAL_PROVIDERS.keys()),
1693
  "endpoints": {
1694
  "GET /": "Home",
1695
  "GET /health": "Health check",
1696
  "GET /v1/models": "List models (NO AUTH)",
1697
+ "POST /v1/messages": "Anthropic format - Full tool support (AUTH, supports stream:true)",
1698
  "POST /v1/messages/stream": "Anthropic format - Stream only (AUTH)",
1699
  "POST /v1/chat/completions": "OpenAI format - Send message (AUTH, supports stream:true)",
1700
  "POST /chat": "Simple chat (AUTH)",
1701
  "POST /chat/stream": "Simple stream (AUTH)",
1702
  "GET /providers": "Providers list (AUTH)",
1703
+ "GET /debug/test-tool-parse": "Test tool call parsing (AUTH)",
1704
  },
1705
  "cookies": COOKIE_STATUS,
1706
  "status": "✅ Server is working"
 
1710
  async def health():
1711
  return {
1712
  "status": "ok",
1713
+ "version": "2.0.0",
1714
  "cookies": COOKIE_STATUS,
1715
  "providers": list(REAL_PROVIDERS.keys()),
1716
  "provider_count": len(REAL_PROVIDERS),
1717
+ "features": {
1718
+ "tool_call_detection": True,
1719
+ "multi_block_content": True,
1720
+ "anthropic_protocol": True
1721
+ },
1722
  "timestamp": int(time.time())
1723
  }
1724
 
 
1734
  }
1735
  return {"providers": result}
1736
 
1737
+ # =====================================================
1738
+ # نقطة تشخيص: اختبار تحليل tool calls
1739
+ # =====================================================
1740
+ @app.get("/debug/test-tool-parse")
1741
+ async def debug_test_tool_parse(request: Request):
1742
+ verify_api_key(request)
1743
+
1744
+ test_cases = [
1745
+ {
1746
+ "name": "Standard tool_call tags",
1747
+ "input": 'I will create the file now.\n<tool_call>{"name": "write_file", "arguments": {"path": "test.py", "content": "print(\'hello\')"}}</tool_call>',
1748
+ },
1749
+ {
1750
+ "name": "Code block format",
1751
+ "input": 'Let me write that.\n```tool_call\n{"name": "read_file", "arguments": {"path": "config.json"}}\n```',
1752
+ },
1753
+ {
1754
+ "name": "Function tag format",
1755
+ "input": 'Creating file:\n<function=write_file>{"path": "app.js", "content": "console.log(1)"}</function>',
1756
+ },
1757
+ {
1758
+ "name": "Multiple tool calls",
1759
+ "input": 'First read, then write.\n<tool_call>{"name": "read_file", "arguments": {"path": "old.txt"}}</tool_call>\nNow writing:\n<tool_call>{"name": "write_file", "arguments": {"path": "new.txt", "content": "data"}}</tool_call>',
1760
+ },
1761
+ {
1762
+ "name": "No tool calls (plain text)",
1763
+ "input": "This is just a regular response with no tool calls.",
1764
+ }
1765
+ ]
1766
+
1767
+ results = []
1768
+ for tc in test_cases:
1769
+ clean_text, tool_calls = ToolCallParser.parse_tool_calls(tc["input"])
1770
+ results.append({
1771
+ "test_name": tc["name"],
1772
+ "input_preview": tc["input"][:100] + "..." if len(tc["input"]) > 100 else tc["input"],
1773
+ "clean_text": clean_text,
1774
+ "tool_calls_found": len(tool_calls),
1775
+ "tool_calls": tool_calls,
1776
+ "has_tool_call_marker": ToolCallParser.might_contain_tool_call(tc["input"])
1777
+ })
1778
+
1779
+ return {"test_results": results}
1780
+
1781
+ # =====================================================
1782
+ # نقطة تشخيص: اختبار stream buffer
1783
+ # =====================================================
1784
+ @app.post("/debug/test-stream-buffer")
1785
+ async def debug_test_stream_buffer(request: Request):
1786
+ verify_api_key(request)
1787
+ body = await request.json()
1788
+
1789
+ text = body.get("text", "")
1790
+ tools = body.get("tools", [])
1791
+ chunk_size = body.get("chunk_size", 10)
1792
+
1793
+ # محاكاة streaming بتقسيم النص إلى chunks
1794
+ buffer = StreamToolBuffer(available_tools=tools)
1795
+ all_events = []
1796
+
1797
+ for i in range(0, len(text), chunk_size):
1798
+ chunk = text[i:i + chunk_size]
1799
+ events = buffer.feed(chunk)
1800
+ for event in events:
1801
+ all_events.append({"chunk_index": i // chunk_size, "event": event})
1802
+
1803
+ # تفريغ المتبقي
1804
+ remaining = buffer.flush()
1805
+ for event in remaining:
1806
+ all_events.append({"chunk_index": "flush", "event": event})
1807
+
1808
+ return {
1809
+ "input_length": len(text),
1810
+ "chunk_size": chunk_size,
1811
+ "total_chunks": (len(text) + chunk_size - 1) // chunk_size,
1812
+ "total_events": len(all_events),
1813
+ "events": all_events
1814
+ }
1815
+
1816
  @app.post("/chat")
1817
  async def chat(request: Request, chat_req: ChatRequest):
1818
  verify_api_key(request)
1819
  result = ""
1820
  for chunk in ask(chat_req.message, chat_req.history, chat_req.provider, chat_req.model):
1821
+ result += chunk
1822
  return JSONResponse({"response": result})
1823
 
1824
  @app.post("/chat/stream")
 
1840
  )
1841
 
1842
  # =====================================================
1843
+ # معالجة الأخطاء العامة - بصيغة Anthropic
1844
  # =====================================================
1845
  @app.exception_handler(404)
1846
  async def not_found_handler(request: Request, exc: HTTPException):
1847
  return JSONResponse(
1848
  status_code=404,
1849
  content={
1850
+ "type": "error",
1851
  "error": {
1852
  "type": "not_found_error",
1853
  "message": f"Endpoint {request.method} {request.url.path} not found"
 
1861
  return JSONResponse(
1862
  status_code=500,
1863
  content={
1864
+ "type": "error",
1865
  "error": {
1866
  "type": "api_error",
1867
  "message": "Internal server error"
 
1869
  }
1870
  )
1871
 
1872
+ @app.exception_handler(401)
1873
+ async def auth_error_handler(request: Request, exc: HTTPException):
1874
+ return JSONResponse(
1875
+ status_code=401,
1876
+ content={
1877
+ "type": "error",
1878
+ "error": {
1879
+ "type": "authentication_error",
1880
+ "message": exc.detail if hasattr(exc, 'detail') else "Invalid API key"
1881
+ }
1882
+ }
1883
+ )
1884
+
1885
+ @app.exception_handler(400)
1886
+ async def bad_request_handler(request: Request, exc: HTTPException):
1887
+ return JSONResponse(
1888
+ status_code=400,
1889
+ content={
1890
+ "type": "error",
1891
+ "error": {
1892
+ "type": "invalid_request_error",
1893
+ "message": exc.detail if hasattr(exc, 'detail') else "Bad request"
1894
+ }
1895
+ }
1896
+ )
1897
+
1898
+ @app.exception_handler(429)
1899
+ async def rate_limit_handler(request: Request, exc: HTTPException):
1900
+ return JSONResponse(
1901
+ status_code=429,
1902
+ content={
1903
+ "type": "error",
1904
+ "error": {
1905
+ "type": "rate_limit_error",
1906
+ "message": "Rate limit exceeded. Please retry after a brief wait."
1907
+ }
1908
+ }
1909
+ )
1910
+
1911
  # =====================================================
1912
  # التشغيل
1913
  # =====================================================
1914
  if __name__ == "__main__":
1915
  import uvicorn
1916
  port = int(os.getenv("PORT", 7860))
1917
+ logger.info(f"Starting G4F Smart Router v2.0 (Protocol Translator) on port {port}")
1918
  logger.info(f"Cookies: {COOKIE_STATUS}")
1919
  logger.info(f"Available providers: {list(REAL_PROVIDERS.keys())}")
1920
+ logger.info(f"Features: Tool Call Detection, Multi-Block Content, Full Anthropic Protocol")
1921
  uvicorn.run("app:app", host="0.0.0.0", port=port, reload=False)