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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +488 -351
app.py CHANGED
@@ -1,4 +1,3 @@
1
-
2
  import os
3
  import json
4
  import time
@@ -25,7 +24,7 @@ logging.basicConfig(
25
  logger = logging.getLogger("g4f-smart-router")
26
 
27
  # =====================================================
28
- # COOKIES (للمزودات التي تحتاجها مثل Perplexity)
29
  # =====================================================
30
  def _load_cookies_raw() -> Dict[str, Any]:
31
  raw_env = (os.getenv("COOKIES_JSON") or "").strip()
@@ -102,7 +101,7 @@ class TTLCache:
102
  CACHE = TTLCache(max_size=100, ttl_seconds=300)
103
 
104
  # =====================================================
105
- # PROVIDERS - قائمة مزودات قد تعمل
106
  # =====================================================
107
  def get_provider(name: str):
108
  try:
@@ -110,7 +109,6 @@ def get_provider(name: str):
110
  except Exception:
111
  return None
112
 
113
- # ترتيب المزودات حسب احتمال النجاح
114
  REAL_PROVIDERS = {
115
  "Blackbox": get_provider("Blackbox"),
116
  "DeepSeek": get_provider("DeepSeek"),
@@ -122,9 +120,6 @@ REAL_PROVIDERS = {
122
  }
123
  REAL_PROVIDERS = {k: v for k, v in REAL_PROVIDERS.items() if v}
124
 
125
- # =====================================================
126
- # MODELS - قائمة نماذج لكل مزود
127
- # =====================================================
128
  PROVIDER_MODELS_FALLBACK = {
129
  "Blackbox": ["gpt-4o", "claude-3.5-sonnet", "llama-3.1-70b", "gemini-pro"],
130
  "DeepSeek": ["deepseek-chat", "deepseek-coder"],
@@ -135,15 +130,11 @@ PROVIDER_MODELS_FALLBACK = {
135
  "Qwen": ["qwen-max", "qwen-plus", "qwen-turbo"],
136
  }
137
 
138
- # =====================================================
139
- # MODEL DISCOVERY
140
- # =====================================================
141
  _PROVIDER_MODEL_CACHE = {}
142
 
143
  def discover_provider_models(provider_obj: Any, provider_name: str) -> List[str]:
144
  if provider_name in _PROVIDER_MODEL_CACHE:
145
  return _PROVIDER_MODEL_CACHE[provider_name]
146
-
147
  candidates = []
148
  for attr in ("models", "model", "default_model", "available_models", "supported_models"):
149
  try:
@@ -157,17 +148,15 @@ def discover_provider_models(provider_obj: Any, provider_name: str) -> List[str]
157
  candidates.append(str(val))
158
  except Exception:
159
  pass
160
-
161
  if not candidates:
162
  candidates = PROVIDER_MODELS_FALLBACK.get(provider_name, ["gpt-4o"])
163
-
164
  seen = set()
165
  unique = [m for m in candidates if not (m in seen or seen.add(m))]
166
  _PROVIDER_MODEL_CACHE[provider_name] = unique
167
  return unique
168
 
169
  # =====================================================
170
- # STREAM CLEANER - لتنظيف شذرات الرد
171
  # =====================================================
172
  def clean_stream(chunk):
173
  try:
@@ -203,8 +192,7 @@ def clean_stream(chunk):
203
  return ""
204
 
205
  # =====================================================
206
- # استخراج النص من content (يدعم string و list)
207
- # إصلاح: Claude يرسل content كـ list من الكائنات
208
  # =====================================================
209
  def extract_text_from_content(content) -> str:
210
  if isinstance(content, str):
@@ -218,8 +206,13 @@ def extract_text_from_content(content) -> str:
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":
@@ -228,15 +221,15 @@ def extract_text_from_content(content) -> 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:
@@ -247,54 +240,57 @@ def extract_text_from_content(content) -> str:
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":',
@@ -307,72 +303,79 @@ class ToolCallParser:
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())
@@ -386,27 +389,26 @@ class ToolCallParser:
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)
@@ -419,17 +421,17 @@ class ToolCallParser:
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())
@@ -440,16 +442,18 @@ class ToolCallParser:
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:
@@ -458,7 +462,10 @@ class ToolCallParser:
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",
@@ -467,34 +474,29 @@ class ToolCallParser:
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", {})
@@ -514,7 +516,6 @@ class ToolCallParser:
514
  if not name:
515
  return None
516
 
517
- # تحويل arguments من string إلى dict إذا لزم
518
  if isinstance(arguments, str):
519
  try:
520
  arguments = json.loads(arguments)
@@ -528,18 +529,18 @@ class ToolCallParser:
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
 
@@ -550,31 +551,25 @@ class ToolCallParser:
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
@@ -587,9 +582,7 @@ class ToolCallParser:
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)):
@@ -598,7 +591,7 @@ class ToolCallParser:
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
 
@@ -606,14 +599,12 @@ class ToolCallParser:
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):
@@ -627,60 +618,68 @@ class StreamToolBuffer:
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:]
@@ -690,26 +689,39 @@ class StreamToolBuffer:
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
 
@@ -718,10 +730,19 @@ class StreamToolBuffer:
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
 
@@ -731,10 +752,12 @@ class StreamToolBuffer:
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
@@ -742,23 +765,20 @@ class StreamToolBuffer:
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
 
@@ -768,34 +788,38 @@ class StreamToolBuffer:
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("")
@@ -808,7 +832,6 @@ class ToolsFormatter:
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
 
@@ -819,32 +842,38 @@ class ToolsFormatter:
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
 
@@ -852,7 +881,10 @@ class ToolsFormatter:
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
@@ -874,32 +906,34 @@ class ToolsFormatter:
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
@@ -911,13 +945,11 @@ class MessageConverter:
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}"
@@ -925,24 +957,23 @@ class MessageConverter:
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
 
@@ -966,18 +997,21 @@ class MessageConverter:
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)
@@ -993,7 +1027,6 @@ class MessageConverter:
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:
@@ -1007,7 +1040,7 @@ class MessageConverter:
1007
 
1008
 
1009
  # =====================================================
1010
- # CHAT LOGIC - مع fallback ذكي
1011
  # =====================================================
1012
  def ask(message: str, history, provider_name: str, model_name: str, stop_flag=None):
1013
  message = (message or "").strip()
@@ -1015,39 +1048,68 @@ def ask(message: str, history, provider_name: str, model_name: str, stop_flag=No
1015
  yield ""
1016
  return
1017
 
1018
- # مفتاح التخزين المؤقت
1019
  key = f"{provider_name}|{model_name}|{message[:200]}"
1020
  cached = CACHE.get(key)
1021
  if cached:
1022
  yield cached
1023
  return
1024
 
1025
- # بناء سجل المحادثة
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
 
1048
  msgs.append({"role": "user", "content": message})
1049
 
1050
- # قائمة المزودات التي سنحاولها
1051
  fallback_providers = [
1052
  provider_name,
1053
  "Blackbox",
@@ -1066,12 +1128,12 @@ def ask(message: str, history, provider_name: str, model_name: str, stop_flag=No
1066
  used.append(pname)
1067
  pobj = REAL_PROVIDERS.get(pname)
1068
  if not pobj:
1069
- logger.info(f"Provider {pname} not available, skipping")
1070
  continue
1071
 
1072
  models_list = discover_provider_models(pobj, pname)
1073
  if not models_list:
1074
- logger.warning(f"No models for provider {pname}")
1075
  continue
1076
 
1077
  if model_name in models_list:
@@ -1081,7 +1143,7 @@ def ask(message: str, history, provider_name: str, model_name: str, stop_flag=No
1081
 
1082
  for m in model_candidates[:10]:
1083
  try:
1084
- logger.info(f"Trying provider {pname} with model {m}")
1085
  stream = g4f.ChatCompletion.create(
1086
  model=m,
1087
  provider=pobj,
@@ -1103,17 +1165,22 @@ def ask(message: str, history, provider_name: str, model_name: str, stop_flag=No
1103
  CACHE.set(key, full)
1104
  return
1105
  except Exception as e:
1106
- logger.warning(f"Provider {pname} model {m} failed: {str(e)[:200]}")
 
 
1107
  continue
1108
 
1109
  yield "❌ فشلت جميع المزودات. تأكد من اتصال الإنترنت أو حاول لاحقاً."
1110
 
 
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(
1118
  CORSMiddleware,
1119
  allow_origins=["*"],
@@ -1124,12 +1191,14 @@ app.add_middleware(
1124
 
1125
  API_KEY = os.getenv("API_KEY", "mysecretkey123")
1126
 
 
1127
  class ChatRequest(BaseModel):
1128
  message: str
1129
  provider: str = "Blackbox"
1130
  model: str = "gpt-4o"
1131
  history: List[Any] = []
1132
 
 
1133
  # =====================================================
1134
  # التحقق من المفتاح
1135
  # =====================================================
@@ -1152,8 +1221,9 @@ def verify_api_key(request: Request):
1152
  detail="Invalid API key. Use 'Authorization: Bearer KEY' or 'X-API-Key: KEY'"
1153
  )
1154
 
 
1155
  # =====================================================
1156
- # دعم HEAD (لإصلاح 405)
1157
  # =====================================================
1158
  @app.head("/")
1159
  async def head_root():
@@ -1175,8 +1245,9 @@ async def head_messages():
1175
  async def head_chat_completions():
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):
@@ -1206,26 +1277,28 @@ async def v1_models(request: Request):
1206
  }]
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:
@@ -1239,68 +1312,64 @@ async def v1_messages(request: Request):
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,
@@ -1316,9 +1385,9 @@ async def v1_messages(request: Request):
1316
  }
1317
  }
1318
 
 
1319
  # =====================================================
1320
  # معالج الـ streaming بصيغة Anthropic SSE
1321
- # مع دعم كامل لـ Tool Use و Multi-Block Content
1322
  # =====================================================
1323
  async def _handle_anthropic_stream(
1324
  full_message: str,
@@ -1350,59 +1419,68 @@ async def _handle_anthropic_stream(
1350
  }
1351
  }
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,
@@ -1413,11 +1491,13 @@ async def _handle_anthropic_stream(
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]
@@ -1429,14 +1509,15 @@ async def _handle_anthropic_stream(
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:
@@ -1444,15 +1525,15 @@ async def _handle_anthropic_stream(
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:
@@ -1465,13 +1546,12 @@ async def _handle_anthropic_stream(
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
@@ -1498,8 +1578,7 @@ async def _handle_anthropic_stream(
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
@@ -1509,29 +1588,26 @@ async def _handle_anthropic_stream(
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": {
1528
- "output_tokens": output_tokens
1529
- }
1530
  }
1531
- yield f"event: message_delta\ndata: {json.dumps(msg_delta, ensure_ascii=False)}\n\n"
 
 
 
1532
 
1533
- # حدث توقف الرسالة
1534
- yield f"event: message_stop\ndata: {{\"type\": \"message_stop\"}}\n\n"
1535
 
1536
  return StreamingResponse(
1537
  generate_stream(),
@@ -1543,8 +1619,9 @@ async def _handle_anthropic_stream(
1543
  }
1544
  )
1545
 
 
1546
  # =====================================================
1547
- # نقطة /v1/messages/stream منفصلة (للتوافق)
1548
  # =====================================================
1549
  @app.post("/v1/messages/stream")
1550
  async def v1_messages_stream(request: Request):
@@ -1570,12 +1647,14 @@ async def v1_messages_stream(request: Request):
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)
@@ -1592,11 +1671,9 @@ async def v1_chat_completions(request: Request):
1592
  model = body.get("model", "gpt-4o")
1593
  is_stream = body.get("stream", False)
1594
 
1595
- # استخراج آخر رسالة
1596
  last_message = messages[-1]
1597
  user_message = extract_text_from_content(last_message.get("content", ""))
1598
 
1599
- # بناء history
1600
  history = []
1601
  for msg in messages[:-1]:
1602
  role = msg.get("role", "user")
@@ -1607,7 +1684,6 @@ async def v1_chat_completions(request: Request):
1607
  completion_id = f"chatcmpl-{int(time.time())}_{os.urandom(4).hex()}"
1608
 
1609
  if is_stream:
1610
- # OpenAI streaming format
1611
  async def openai_stream():
1612
  for chunk in ask(user_message, history, "Blackbox", model):
1613
  if chunk:
@@ -1624,7 +1700,6 @@ async def v1_chat_completions(request: Request):
1624
  }
1625
  yield f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
1626
 
1627
- # إرسال حدث النهاية
1628
  final_data = {
1629
  "id": completion_id,
1630
  "object": "chat.completion.chunk",
@@ -1649,7 +1724,6 @@ async def v1_chat_completions(request: Request):
1649
  }
1650
  )
1651
 
1652
- # Non-stream OpenAI format
1653
  full_response = ""
1654
  for chunk in ask(user_message, history, "Blackbox", model):
1655
  full_response += chunk
@@ -1674,6 +1748,7 @@ async def v1_chat_completions(request: Request):
1674
  }
1675
  }
1676
 
 
1677
  # =====================================================
1678
  # نقاط نهاية إضافية
1679
  # =====================================================
@@ -1681,47 +1756,47 @@ async def v1_chat_completions(request: Request):
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"
1707
  }
1708
 
 
1709
  @app.get("/health")
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
 
 
1725
  @app.get("/providers")
1726
  async def get_providers(request: Request):
1727
  verify_api_key(request)
@@ -1734,6 +1809,7 @@ async def get_providers(request: Request):
1734
  }
1735
  return {"providers": result}
1736
 
 
1737
  # =====================================================
1738
  # نقطة تشخيص: اختبار تحليل tool calls
1739
  # =====================================================
@@ -1744,19 +1820,58 @@ async def debug_test_tool_parse(request: Request):
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)",
@@ -1769,7 +1884,11 @@ async def debug_test_tool_parse(request: Request):
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,
@@ -1778,6 +1897,7 @@ async def debug_test_tool_parse(request: Request):
1778
 
1779
  return {"test_results": results}
1780
 
 
1781
  # =====================================================
1782
  # نقطة تشخيص: اختبار stream buffer
1783
  # =====================================================
@@ -1790,7 +1910,6 @@ async def debug_test_stream_buffer(request: Request):
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
 
@@ -1800,7 +1919,6 @@ async def debug_test_stream_buffer(request: Request):
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})
@@ -1813,20 +1931,26 @@ async def debug_test_stream_buffer(request: Request):
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")
1825
  async def chat_stream(request: Request, chat_req: ChatRequest):
1826
  verify_api_key(request)
1827
 
1828
  async def generate():
1829
- for chunk in ask(chat_req.message, chat_req.history, chat_req.provider, chat_req.model):
 
 
1830
  yield f"data: {json.dumps({'delta': chunk}, ensure_ascii=False)}\n\n"
1831
  yield "data: [DONE]\n\n"
1832
 
@@ -1839,8 +1963,9 @@ async def chat_stream(request: Request, chat_req: ChatRequest):
1839
  }
1840
  )
1841
 
 
1842
  # =====================================================
1843
- # معالجة الأخطاء العامة - بصيغة Anthropic
1844
  # =====================================================
1845
  @app.exception_handler(404)
1846
  async def not_found_handler(request: Request, exc: HTTPException):
@@ -1850,11 +1975,14 @@ async def not_found_handler(request: Request, exc: HTTPException):
1850
  "type": "error",
1851
  "error": {
1852
  "type": "not_found_error",
1853
- "message": f"Endpoint {request.method} {request.url.path} not found"
 
 
1854
  }
1855
  }
1856
  )
1857
 
 
1858
  @app.exception_handler(500)
1859
  async def internal_error_handler(request: Request, exc: Exception):
1860
  logger.error(f"Internal error: {str(exc)}")
@@ -1869,6 +1997,7 @@ async def internal_error_handler(request: Request, exc: Exception):
1869
  }
1870
  )
1871
 
 
1872
  @app.exception_handler(401)
1873
  async def auth_error_handler(request: Request, exc: HTTPException):
1874
  return JSONResponse(
@@ -1877,11 +2006,14 @@ async def auth_error_handler(request: Request, exc: HTTPException):
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(
@@ -1890,11 +2022,14 @@ async def bad_request_handler(request: Request, exc: HTTPException):
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(
@@ -1908,14 +2043,16 @@ async def rate_limit_handler(request: Request, exc: HTTPException):
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)
 
 
 
1
  import os
2
  import json
3
  import time
 
24
  logger = logging.getLogger("g4f-smart-router")
25
 
26
  # =====================================================
27
+ # COOKIES
28
  # =====================================================
29
  def _load_cookies_raw() -> Dict[str, Any]:
30
  raw_env = (os.getenv("COOKIES_JSON") or "").strip()
 
101
  CACHE = TTLCache(max_size=100, ttl_seconds=300)
102
 
103
  # =====================================================
104
+ # PROVIDERS
105
  # =====================================================
106
  def get_provider(name: str):
107
  try:
 
109
  except Exception:
110
  return None
111
 
 
112
  REAL_PROVIDERS = {
113
  "Blackbox": get_provider("Blackbox"),
114
  "DeepSeek": get_provider("DeepSeek"),
 
120
  }
121
  REAL_PROVIDERS = {k: v for k, v in REAL_PROVIDERS.items() if v}
122
 
 
 
 
123
  PROVIDER_MODELS_FALLBACK = {
124
  "Blackbox": ["gpt-4o", "claude-3.5-sonnet", "llama-3.1-70b", "gemini-pro"],
125
  "DeepSeek": ["deepseek-chat", "deepseek-coder"],
 
130
  "Qwen": ["qwen-max", "qwen-plus", "qwen-turbo"],
131
  }
132
 
 
 
 
133
  _PROVIDER_MODEL_CACHE = {}
134
 
135
  def discover_provider_models(provider_obj: Any, provider_name: str) -> List[str]:
136
  if provider_name in _PROVIDER_MODEL_CACHE:
137
  return _PROVIDER_MODEL_CACHE[provider_name]
 
138
  candidates = []
139
  for attr in ("models", "model", "default_model", "available_models", "supported_models"):
140
  try:
 
148
  candidates.append(str(val))
149
  except Exception:
150
  pass
 
151
  if not candidates:
152
  candidates = PROVIDER_MODELS_FALLBACK.get(provider_name, ["gpt-4o"])
 
153
  seen = set()
154
  unique = [m for m in candidates if not (m in seen or seen.add(m))]
155
  _PROVIDER_MODEL_CACHE[provider_name] = unique
156
  return unique
157
 
158
  # =====================================================
159
+ # STREAM CLEANER
160
  # =====================================================
161
  def clean_stream(chunk):
162
  try:
 
192
  return ""
193
 
194
  # =====================================================
195
+ # استخراج النص من content
 
196
  # =====================================================
197
  def extract_text_from_content(content) -> str:
198
  if isinstance(content, str):
 
206
  if item.get("type") == "text":
207
  parts.append(item.get("text", ""))
208
  elif item.get("type") == "tool_result":
 
209
  tool_content = item.get("content", "")
210
+ tool_use_id = item.get("tool_use_id", "")
211
+ # ======================================================
212
+ # [إصلاح #3] - ربط نتيجة الأداة بمعرفها tool_use_id
213
+ # ======================================================
214
+ if tool_use_id:
215
+ parts.append(f"[Tool Result for {tool_use_id}]:")
216
  if isinstance(tool_content, list):
217
  for tc in tool_content:
218
  if isinstance(tc, dict) and tc.get("type") == "text":
 
221
  parts.append(tc)
222
  elif isinstance(tool_content, str):
223
  parts.append(tool_content)
 
 
 
 
224
  elif item.get("type") == "tool_use":
 
225
  tool_name = item.get("name", "unknown")
226
  tool_input = item.get("input", {})
227
+ # الحفاظ على tool_id لربط الاستجابة لاحقاً
228
+ tool_id = item.get("id", "")
229
+ parts.append(
230
+ f"[Called tool: {tool_name} (id:{tool_id}) "
231
+ f"with input: {json.dumps(tool_input, ensure_ascii=False)}]"
232
+ )
233
  elif "text" in item:
234
  parts.append(item["text"])
235
  elif "content" in item:
 
240
  return ""
241
 
242
  # =====================================================
243
+ # TOOL CALL PARSER
 
244
  # =====================================================
245
  class ToolCallParser:
246
  """
247
+ يكتشف ويحلل وسوم Tool Call من استجابة النموذج.
248
+ يدعم أنماطاً متعددة مع regex محسّن يقبل المسافات والأسطر الجديدة.
 
 
 
 
249
  """
250
 
251
+ # ======================================================
252
+ # [إصلاح #2] - تحديث الـ Regex ليكون non-greedy
253
+ # ويقبل المسافات البيضاء والأسطر الجديدة قبل وبعد الوسوم
254
+ # ======================================================
255
  PATTERNS = [
256
+ # النمط 1: <tool_call> مع مسافات مرنة حول الوسم والـ JSON
257
  re.compile(
258
+ r'\s*<tool_call>\s*(\{.*?\})\s*</tool_call>\s*',
259
+ re.DOTALL | re.IGNORECASE
260
  ),
261
+ # النمط 2: ```tool_call أو ```json مع مسافات مرنة
262
  re.compile(
263
+ r'```(?:tool_call|json)?\s*\n?\s*(\{.*?"(?:name|function)".*?\})\s*\n?\s*```',
264
+ re.DOTALL | re.IGNORECASE
265
  ),
266
  # النمط 3: <function=name>JSON</function>
267
  re.compile(
268
+ r'\s*<function=(\w+)>\s*(\{.*?\})\s*</function>\s*',
269
+ re.DOTALL | re.IGNORECASE
270
  ),
271
  # النمط 4: [TOOL_CALL]...[/TOOL_CALL]
272
  re.compile(
273
+ r'\s*\[TOOL_CALL\]\s*(.*?)\s*\[/TOOL_CALL\]\s*',
274
+ re.DOTALL | re.IGNORECASE
275
  ),
276
+ # النمط 5: ✿FUNCTION✿
277
  re.compile(
278
  r'✿FUNCTION✿:\s*(\w+)\s*\n✿ARGS✿:\s*(\{.*?\})\s*(?:\n✿RESULT✿)?',
279
  re.DOTALL
280
  ),
281
  ]
282
 
283
+ # ======================================================
284
+ # [إصلاح #2] - علامات البداية محدّثة لتشمل المسافات
285
+ # ======================================================
286
  START_MARKERS = [
287
  '<tool_call>',
288
  '```tool_call',
289
+ '```json',
290
  '<function=',
291
+ '[tool_call]', # نسخة lowercase للمرونة
292
  '[TOOL_CALL]',
293
+ '✿function✿',
294
  '✿FUNCTION✿',
295
  '{"tool_calls"',
296
  '"tool_calls":',
 
303
 
304
  @classmethod
305
  def might_contain_tool_call(cls, text: str) -> bool:
306
+ """
307
+ فحص سريع: هل النص قد يحتوي على بداية tool call؟
308
+ [إصلاح #2] - يتجاهل حالة الأحرف ويتعامل مع المسافات
309
+ """
310
+ # تنظيف النص من المسافات الزائدة للفحص
311
+ text_stripped = text.strip().lower()
312
  for marker in cls.START_MARKERS:
313
+ if marker.lower() in text_stripped:
314
  return True
315
+ # فحص إضافي: هل يبدأ بـ { ويحتوي "name"؟ (JSON مباشر)
316
+ if text_stripped.startswith('{') and '"name"' in text_stripped:
317
+ return True
318
  return False
319
 
320
  @classmethod
321
+ def parse_tool_calls(
322
+ cls,
323
+ text: str,
324
+ available_tools: Optional[List[Dict]] = None
325
+ ) -> Tuple[str, List[Dict]]:
326
  """
327
  تحليل النص واستخراج tool calls منه.
328
+ [إصلاح #2] - regex محسّن مع non-greedy matching
 
 
 
329
  """
330
  tool_calls = []
331
  clean_text = text
332
 
333
+ # ---- النمط 1: <tool_call>JSON</tool_call> ----
334
+ pattern1 = re.compile(
335
+ r'\s*<tool_call>\s*(\{.*?\})\s*</tool_call>\s*',
336
+ re.DOTALL | re.IGNORECASE
337
  )
338
+ for match in pattern1.finditer(text):
339
  try:
340
  raw_json = match.group(1).strip()
341
  parsed = json.loads(raw_json)
342
  tool_call = cls._normalize_tool_call(parsed, available_tools)
343
  if tool_call:
344
  tool_calls.append(tool_call)
345
+ clean_text = clean_text.replace(match.group(0), " ", 1)
346
  except json.JSONDecodeError as e:
347
+ logger.warning(f"[Parser] Failed to parse tool_call JSON: {e}")
348
+ fixed = cls._try_fix_json(match.group(1).strip())
 
349
  if fixed:
350
  tool_call = cls._normalize_tool_call(fixed, available_tools)
351
  if tool_call:
352
  tool_calls.append(tool_call)
353
+ clean_text = clean_text.replace(match.group(0), " ", 1)
354
 
355
+ # ---- النمط 2: ```tool_call\nJSON\n``` ----
356
  if not tool_calls:
357
+ pattern2 = re.compile(
358
+ r'```(?:tool_call|json)?\s*\n?\s*(\{.*?"(?:name|function)".*?\})\s*\n?\s*```',
359
+ re.DOTALL | re.IGNORECASE
360
  )
361
+ for match in pattern2.finditer(text):
362
  try:
363
  raw_json = match.group(1).strip()
364
  parsed = json.loads(raw_json)
365
  tool_call = cls._normalize_tool_call(parsed, available_tools)
366
  if tool_call:
367
  tool_calls.append(tool_call)
368
+ clean_text = clean_text.replace(match.group(0), " ", 1)
369
  except json.JSONDecodeError:
370
  pass
371
 
372
+ # ---- النمط 3: <function=name>JSON</function> ----
373
  if not tool_calls:
374
+ pattern3 = re.compile(
375
+ r'\s*<function=(\w+)>\s*(\{.*?\})\s*</function>\s*',
376
+ re.DOTALL | re.IGNORECASE
377
  )
378
+ for match in pattern3.finditer(text):
379
  try:
380
  func_name = match.group(1)
381
  args_json = json.loads(match.group(2).strip())
 
389
  tool_call = cls._validate_against_tools(tool_call, available_tools)
390
  if tool_call:
391
  tool_calls.append(tool_call)
392
+ clean_text = clean_text.replace(match.group(0), " ", 1)
393
  except json.JSONDecodeError:
394
  pass
395
 
396
+ # ---- النمط 4: [TOOL_CALL]...[/TOOL_CALL] ----
397
  if not tool_calls:
398
+ pattern4 = re.compile(
399
+ r'\s*\[TOOL_CALL\]\s*(.*?)\s*\[/TOOL_CALL\]\s*',
400
+ re.DOTALL | re.IGNORECASE
401
  )
402
+ for match in pattern4.finditer(text):
403
+ content_inner = match.group(1).strip()
404
  try:
405
+ parsed = json.loads(content_inner)
406
  tool_call = cls._normalize_tool_call(parsed, available_tools)
407
  if tool_call:
408
  tool_calls.append(tool_call)
409
+ clean_text = clean_text.replace(match.group(0), " ", 1)
410
  except json.JSONDecodeError:
411
+ func_match = re.match(r'(\w+)\s*\((.*)\)', content_inner, re.DOTALL)
 
412
  if func_match:
413
  try:
414
  func_name = func_match.group(1)
 
421
  "input": args
422
  }
423
  tool_calls.append(tool_call)
424
+ clean_text = clean_text.replace(match.group(0), " ", 1)
425
  except json.JSONDecodeError:
426
  pass
427
 
428
+ # ---- النمط 5: ✿FUNCTION✿ ----
429
  if not tool_calls:
430
+ pattern5 = re.compile(
431
  r'✿FUNCTION✿:\s*(\w+)\s*\n✿ARGS✿:\s*(\{.*?\})\s*(?:\n✿RESULT✿)?',
432
+ re.DOTALL
433
  )
434
+ for match in pattern5.finditer(text):
435
  try:
436
  func_name = match.group(1)
437
  args_json = json.loads(match.group(2).strip())
 
442
  "input": args_json
443
  }
444
  tool_calls.append(tool_call)
445
+ clean_text = clean_text.replace(match.group(0), " ", 1)
446
  except json.JSONDecodeError:
447
  pass
448
 
449
+ # ---- النمط 6: JSON مباشر يحتوي tool_calls ----
450
  if not tool_calls:
451
  try:
452
+ json_pattern = re.compile(
453
+ r'\{[^{}]*"tool_calls"[^{}]*\[.*?\]\s*\}',
454
+ re.DOTALL
455
+ )
456
+ for jm in json_pattern.finditer(text):
457
  try:
458
  parsed = json.loads(jm.group(0))
459
  if "tool_calls" in parsed:
 
462
  name = func_data.get("name", "")
463
  args = func_data.get("arguments", func_data.get("input", {}))
464
  if isinstance(args, str):
465
+ try:
466
+ args = json.loads(args)
467
+ except json.JSONDecodeError:
468
+ args = {"raw": args}
469
  if name:
470
  tool_call = {
471
  "type": "tool_use",
 
474
  "input": args
475
  }
476
  tool_calls.append(tool_call)
477
+ clean_text = clean_text.replace(jm.group(0), " ", 1)
478
  except json.JSONDecodeError:
479
  pass
480
  except Exception:
481
  pass
482
 
483
+ # تنظيف النص
484
  clean_text = clean_text.strip()
 
485
  clean_text = re.sub(r'\n{3,}', '\n\n', clean_text)
486
+ clean_text = re.sub(r' +', ' ', clean_text)
487
 
488
  return clean_text, tool_calls
489
 
490
  @classmethod
491
+ def _normalize_tool_call(
492
+ cls,
493
+ parsed: Dict,
494
+ available_tools: Optional[List[Dict]] = None
495
+ ) -> Optional[Dict]:
496
+ """توحيد صيغة tool call من أشكال مختلفة إلى صيغة Anthropic"""
 
 
 
 
497
  name = None
498
  arguments = {}
499
 
 
500
  if "function" in parsed and isinstance(parsed["function"], dict):
501
  name = parsed["function"].get("name")
502
  arguments = parsed["function"].get("arguments", {})
 
516
  if not name:
517
  return None
518
 
 
519
  if isinstance(arguments, str):
520
  try:
521
  arguments = json.loads(arguments)
 
529
  "input": arguments if isinstance(arguments, dict) else {"value": arguments}
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(
539
+ cls,
540
+ tool_call: Dict,
541
+ available_tools: List[Dict]
542
+ ) -> Optional[Dict]:
543
+ """التحقق من أن الأداة موجودة في القائمة المتاحة"""
544
  requested_name = tool_call["name"]
545
  tool_names = []
546
 
 
551
  tool_names.append(tool_name)
552
 
553
  if tool_name == requested_name:
 
554
  return tool_call
555
 
 
556
  requested_lower = requested_name.lower().replace("_", "").replace("-", "")
557
  for tool_name in tool_names:
558
  tool_lower = tool_name.lower().replace("_", "").replace("-", "")
559
  if requested_lower == tool_lower:
560
+ tool_call["name"] = tool_name
561
  return tool_call
 
562
  if requested_lower in tool_lower or tool_lower in requested_lower:
563
  tool_call["name"] = tool_name
564
  return tool_call
565
 
566
+ logger.warning(f"[Validator] Tool '{requested_name}' not found in: {tool_names}")
 
567
  return tool_call
568
 
569
  @classmethod
570
  def _try_fix_json(cls, broken_json: str) -> Optional[Dict]:
571
  """محاولة إصلاح JSON مكسور"""
 
572
  fixed = re.sub(r',\s*([}\]])', r'\1', broken_json)
 
573
  open_braces = fixed.count('{') - fixed.count('}')
574
  if open_braces > 0:
575
  fixed += '}' * open_braces
 
582
  except json.JSONDecodeError:
583
  pass
584
 
 
585
  try:
 
586
  start = fixed.index('{')
587
  depth = 0
588
  for i in range(start, len(fixed)):
 
591
  elif fixed[i] == '}':
592
  depth -= 1
593
  if depth == 0:
594
+ return json.loads(fixed[start:i + 1])
595
  except (ValueError, json.JSONDecodeError):
596
  pass
597
 
 
599
 
600
 
601
  # =====================================================
602
+ # STREAM TOOL BUFFER - محسّن
603
  # =====================================================
604
  class StreamToolBuffer:
605
  """
606
  Buffer ذكي يجمع chunks الـ stream ويكتشف tool calls.
607
+ [إصلاح #2] - تحديث _process_text_mode لاكتشاف أفضل
 
 
608
  """
609
 
610
  def __init__(self, available_tools: Optional[List[Dict]] = None):
 
618
  def feed(self, chunk: str) -> List[Dict]:
619
  """
620
  إطعام chunk جديد للـ buffer.
621
+ يُرجع قائمة من الأحداث الجاهزة للإرسال.
 
 
 
 
622
  """
623
  events = []
624
  self.buffer += chunk
625
 
626
  while self.buffer:
627
  if self.in_tool_call:
 
628
  events.extend(self._process_tool_call_mode())
629
  if self.in_tool_call:
630
+ break
631
  else:
 
632
  events.extend(self._process_text_mode())
633
  if self.in_tool_call:
634
+ continue
635
+ break
636
 
637
  return events
638
 
639
  def _process_text_mode(self) -> List[Dict]:
640
+ """
641
+ معالجة النص في النمط العادي.
642
+ [إصلاح #2] - يتجاهل حالة الأحرف ويتعامل مع المسافات قبل الوسوم
643
+ """
644
  events = []
645
 
646
+ # ======================================================
647
+ # [إصلاح #2] البحث بدون حساسية لحالة الأحرف
648
+ # ======================================================
649
+ buffer_lower = self.buffer.lower()
650
+
651
  earliest_pos = -1
652
  earliest_marker = ""
653
+
654
  for marker in ToolCallParser.START_MARKERS:
655
+ # البحث بدون حساسية لحالة الأحرف
656
+ pos = buffer_lower.find(marker.lower())
657
  if pos != -1 and (earliest_pos == -1 or pos < earliest_pos):
658
  earliest_pos = pos
659
  earliest_marker = marker
660
 
661
  if earliest_pos == -1:
662
+ # لا يوجد tool call - احتفظ بـ 100 حرف كاحتياط (زيادة من 50)
663
+ # لأن بعض الوسوم قد تكون أطول
664
+ safe_length = len(self.buffer) - 100
665
  if safe_length > 0:
666
  text_to_send = self.buffer[:safe_length]
667
  self.buffer = self.buffer[safe_length:]
668
  if text_to_send:
669
  events.append({"type": "text", "text": text_to_send})
 
670
  else:
671
+ # ======================================================
672
+ # [إصلاح #2] تخطي المسافات البيضاء قبل الوسم
673
+ # ======================================================
674
+ # إرسال النص قبل الوسم (مع إزالة المسافات الزائدة قبله)
675
+ text_before = self.buffer[:earliest_pos]
676
+
677
+ # لا نرسل إذا كان النص قبله مجرد مسافات
678
+ if text_before and not text_before.isspace():
679
+ events.append({"type": "text", "text": text_before})
680
+ elif text_before and text_before.isspace():
681
+ # مسافات قبل الوسم - نرسلها كمسافة واحدة
682
+ events.append({"type": "text", "text": " "})
683
 
684
  # الانتقال لنمط tool call
685
  self.buffer = self.buffer[earliest_pos:]
 
689
  return events
690
 
691
  def _process_tool_call_mode(self) -> List[Dict]:
692
+ """
693
+ معالجة النص في نمط tool call.
694
+ [إصلاح #2] - تحسين اكتشاف نهاية الوسوم مع المسافات
695
+ """
696
  events = []
697
 
698
+ # أزواج (نهاية الوسم، بداية الوسم) مع دعم حالة الأحرف
699
+ end_markers_pairs = [
700
  ('</tool_call>', '<tool_call>'),
701
  ('```', '```tool_call'),
702
  ('```', '```json'),
703
  ('</function>', '<function='),
704
+ ('[/tool_call]', '[tool_call]'),
705
  ('[/TOOL_CALL]', '[TOOL_CALL]'),
706
  ]
707
 
708
+ buffer_lower = self.buffer.lower()
709
  found_end = False
710
+
711
+ for end_marker, start_marker in end_markers_pairs:
712
+ end_lower = end_marker.lower()
713
+ start_lower = start_marker.lower()
714
+
715
+ # التحقق أن الـ buffer يبدأ بعلامة البداية (بدون حساسية)
716
+ if not buffer_lower.startswith(start_lower):
717
+ continue
718
+
719
+ # البحث عن نهاية الوسم بعد البداية
720
+ search_from = len(start_marker)
721
+ end_pos = buffer_lower.find(end_lower, search_from)
722
 
723
  if end_pos != -1:
724
+ # وجدنا النهاية - استخراج النص الكامل للوسم
725
  full_tool_text = self.buffer[:end_pos + len(end_marker)]
726
  remaining = self.buffer[end_pos + len(end_marker):]
727
 
 
730
  full_tool_text, self.available_tools
731
  )
732
 
733
+ # ======================================================
734
+ # [إصلاح #3] إرسال tool calls مع تأكيد المعرف
735
+ # ======================================================
736
  for tc in tool_calls:
737
+ # التأكد من وجود معرف صحيح
738
+ if not tc.get("id"):
739
+ tc["id"] = ToolCallParser.generate_tool_id()
740
+ logger.info(
741
+ f"[Buffer] Tool call detected: {tc['name']} "
742
+ f"(id: {tc['id']})"
743
+ )
744
  events.append(tc)
745
 
 
746
  if clean_text.strip():
747
  events.append({"type": "text", "text": clean_text})
748
 
 
752
  break
753
 
754
  if not found_end:
755
+ # لم نجد نهاية - تحقق من الحجم
756
  if len(self.buffer) > 5000:
757
+ logger.warning(
758
+ "[Buffer] Tool call buffer too large without closing tag, "
759
+ "treating as text"
760
+ )
761
  events.append({"type": "text", "text": self.buffer})
762
  self.buffer = ""
763
  self.in_tool_call = False
 
765
  return events
766
 
767
  def flush(self) -> List[Dict]:
768
+ """تفريغ أي محتوى متبقي في الـ buffer"""
 
 
 
769
  events = []
770
 
771
  if self.in_tool_call and self.buffer:
 
772
  clean_text, tool_calls = ToolCallParser.parse_tool_calls(
773
  self.buffer, self.available_tools
774
  )
775
  for tc in tool_calls:
776
+ if not tc.get("id"):
777
+ tc["id"] = ToolCallParser.generate_tool_id()
778
  events.append(tc)
779
  if clean_text.strip():
780
  events.append({"type": "text", "text": clean_text})
781
  elif self.buffer:
 
782
  if self.buffer.strip():
783
  events.append({"type": "text", "text": self.buffer})
784
 
 
788
 
789
 
790
  # =====================================================
791
+ # TOOLS FORMATTER
792
  # =====================================================
793
  class ToolsFormatter:
794
+ """تحويل أدوات Anthropic إلى نص للنموذج"""
 
 
 
795
 
796
  @staticmethod
797
  def format_tools_for_prompt(tools: List[Dict], tool_choice: Any = None) -> str:
 
 
 
798
  if not tools:
799
  return ""
800
 
801
  lines = []
802
  lines.append("# Available Tools")
803
  lines.append("")
804
+ lines.append(
805
+ "You have access to the following tools. "
806
+ "To use a tool, respond with a tool_call block."
807
+ )
808
+ lines.append(
809
+ "IMPORTANT: When you need to use a tool, you MUST format "
810
+ "your response EXACTLY like this:"
811
+ )
812
  lines.append("")
813
  lines.append("<tool_call>")
814
+ lines.append('{"name": "tool_name", "arguments": {"param1": "value1"}}')
815
  lines.append("</tool_call>")
816
  lines.append("")
817
+ lines.append(
818
+ "You can include text explanation before and/or after the tool_call block."
819
+ )
820
+ lines.append(
821
+ "You can make multiple tool calls by using multiple <tool_call> blocks."
822
+ )
823
  lines.append("")
824
  lines.append("## Tool Definitions:")
825
  lines.append("")
 
832
  lines.append(f"### {i}. `{name}`")
833
  lines.append(f"**Description:** {description}")
834
 
 
835
  properties = input_schema.get("properties", {})
836
  required = input_schema.get("required", [])
837
 
 
842
  param_desc = param_info.get("description", "")
843
  is_required = param_name in required
844
  req_marker = " (required)" if is_required else " (optional)"
845
+ lines.append(
846
+ f" - `{param_name}` ({param_type}{req_marker}): {param_desc}"
847
+ )
848
  if "enum" in param_info:
849
+ lines.append(
850
+ f" Allowed values: "
851
+ f"{', '.join(str(v) for v in param_info['enum'])}"
852
+ )
853
  else:
854
  lines.append("**Parameters:** None")
855
 
856
  lines.append("")
857
 
 
858
  if tool_choice:
859
  if isinstance(tool_choice, dict):
860
  if tool_choice.get("type") == "tool":
861
  forced_tool = tool_choice.get("name", "")
862
  if forced_tool:
863
+ lines.append(
864
+ f"**IMPORTANT:** You MUST use the `{forced_tool}` tool."
865
+ )
866
  elif tool_choice.get("type") == "any":
867
+ lines.append(
868
+ "**IMPORTANT:** You MUST use at least one tool."
869
+ )
870
  elif tool_choice == "auto":
871
+ lines.append("Use tools when appropriate.")
872
  elif tool_choice == "any":
873
+ lines.append("**IMPORTANT:** You MUST use at least one tool.")
874
 
875
  lines.append("")
876
+ lines.append("Remember: Always use <tool_call>JSON</tool_call> format.")
877
  lines.append("The JSON inside must have 'name' and 'arguments' keys.")
878
  lines.append("")
879
 
 
881
 
882
  @staticmethod
883
  def format_tool_result_for_message(tool_use_id: str, content: Any) -> str:
884
+ """
885
+ تحويل نتيجة أداة إلى نص مفهوم للنموذج.
886
+ [إصلاح #3] - ربط النتيجة بمعرف الأداة tool_use_id
887
+ """
888
  result_text = ""
889
  if isinstance(content, str):
890
  result_text = content
 
906
  else:
907
  result_text = str(content) if content else ""
908
 
909
+ # ======================================================
910
+ # [إصلاح #3] - تأكيد ربط النتيجة بالمعرف الصحيح
911
+ # ======================================================
912
+ return (
913
+ f"[Tool Result]\n"
914
+ f"tool_use_id: {tool_use_id}\n"
915
+ f"status: success\n"
916
+ f"output:\n{result_text}\n"
917
+ f"[/Tool Result]"
918
+ )
919
 
920
 
921
  # =====================================================
922
+ # MESSAGE CONVERTER
923
  # =====================================================
924
  class MessageConverter:
925
+ """تحويل رسائل Anthropic إلى صيغة g4f"""
 
 
 
926
 
927
  @staticmethod
928
+ def convert_messages(
929
+ messages: List[Dict],
930
+ system_prompt: str = "",
931
+ tools: Optional[List[Dict]] = None,
932
+ tool_choice: Any = None
933
+ ) -> Tuple[str, List[Dict]]:
934
+ """تحويل رسائل Anthropic إلى (full_message, history) لـ g4f"""
 
 
 
935
  history = []
936
+
 
937
  full_system = ""
938
  if system_prompt:
939
  full_system = system_prompt
 
945
  else:
946
  full_system = tools_text
947
 
 
948
  for msg in messages:
949
  role = msg.get("role", "user")
950
  content = msg.get("content", "")
951
 
952
  if role == "system":
 
953
  sys_text = extract_text_from_content(content)
954
  if full_system:
955
  full_system = f"{full_system}\n\n{sys_text}"
 
957
  full_system = sys_text
958
  continue
959
 
 
960
  converted_text = MessageConverter._convert_content(content, role)
961
 
962
  if converted_text:
 
963
  g4f_role = "user" if role == "user" else "assistant"
964
  history.append({"role": g4f_role, "content": converted_text})
965
 
 
966
  if history:
967
  last_msg = history.pop()
968
  user_message = last_msg["content"]
969
  else:
970
  user_message = ""
971
 
 
972
  if full_system:
973
+ full_message = (
974
+ f"[System Instructions]\n{full_system}\n"
975
+ f"[/System Instructions]\n\n{user_message}"
976
+ )
977
  else:
978
  full_message = user_message
979
 
 
997
  parts.append(block.get("text", ""))
998
 
999
  elif block_type == "tool_use":
 
1000
  name = block.get("name", "unknown")
1001
  input_data = block.get("input", {})
1002
  tool_id = block.get("id", "")
1003
  parts.append(
1004
  f"<tool_call>"
1005
+ f'{{"name": "{name}", '
1006
+ f'"id": "{tool_id}", '
1007
+ f'"arguments": {json.dumps(input_data, ensure_ascii=False)}}}'
1008
  f"</tool_call>"
1009
  )
1010
 
1011
  elif block_type == "tool_result":
1012
+ # ======================================================
1013
+ # [إصلاح #3] - ربط نتيجة الأداة بمعرفها الصحيح
1014
+ # ======================================================
1015
  tool_use_id = block.get("tool_use_id", "")
1016
  result_content = block.get("content", "")
1017
  is_error = block.get("is_error", False)
 
1027
  parts.append("[Image content - not supported in text mode]")
1028
 
1029
  else:
 
1030
  if "text" in block:
1031
  parts.append(block["text"])
1032
  else:
 
1040
 
1041
 
1042
  # =====================================================
1043
+ # CHAT LOGIC - مع إصلاح الذاكرة الانتقائية
1044
  # =====================================================
1045
  def ask(message: str, history, provider_name: str, model_name: str, stop_flag=None):
1046
  message = (message or "").strip()
 
1048
  yield ""
1049
  return
1050
 
 
1051
  key = f"{provider_name}|{model_name}|{message[:200]}"
1052
  cached = CACHE.get(key)
1053
  if cached:
1054
  yield cached
1055
  return
1056
 
 
1057
  msgs = []
1058
  try:
1059
  if history:
1060
  if isinstance(history, list) and len(history) > 0:
1061
  if isinstance(history[0], dict):
1062
+ # ======================================================
1063
+ # [إصلاح #1] - الذاكرة الانتقائية (Selective History)
1064
+ # الاحتفاظ بأول 10 رسائل (System Prompt + Tool Definitions)
1065
+ # ودمجها مع آخر 50 رسالة للسياق الحالي
1066
+ # السطر 1031 المشار إليه في التقرير
1067
+ # ======================================================
1068
+ if len(history) > 60:
1069
+ # الاحتفاظ بأول 10 رسائل + آخر 50 رسالة
1070
+ smart_history = history[:10] + history[-50:]
1071
+ logger.info(
1072
+ f"[Memory] Smart history: keeping first 10 + "
1073
+ f"last 50 from {len(history)} total messages"
1074
+ )
1075
+ else:
1076
+ # إذا كانت الرسائل أقل من 60، نأخذها كلها
1077
+ smart_history = history
1078
+ logger.info(
1079
+ f"[Memory] Full history: {len(history)} messages "
1080
+ f"(under threshold)"
1081
+ )
1082
+
1083
+ for item in smart_history:
1084
  role = item.get("role")
1085
  content = item.get("content")
1086
  if role and content:
1087
+ text = (
1088
+ extract_text_from_content(content)
1089
+ if not isinstance(content, str)
1090
+ else content
1091
+ )
1092
  if text:
1093
  msgs.append({"role": str(role), "content": text})
1094
  else:
1095
+ # صيغة قديمة (list of tuples)
1096
+ if len(history) > 30:
1097
+ smart_history_tuples = history[:5] + history[-25:]
1098
+ else:
1099
+ smart_history_tuples = history
1100
+
1101
+ for item in smart_history_tuples:
1102
  if isinstance(item, (list, tuple)) and len(item) == 2:
1103
  if item[0]:
1104
  msgs.append({"role": "user", "content": str(item[0])})
1105
  if item[1]:
1106
  msgs.append({"role": "assistant", "content": str(item[1])})
1107
  except Exception as e:
1108
+ logger.warning(f"[Memory] History error: {e}")
1109
 
1110
  msgs.append({"role": "user", "content": message})
1111
 
1112
+ # قائمة المزودات للـ fallback
1113
  fallback_providers = [
1114
  provider_name,
1115
  "Blackbox",
 
1128
  used.append(pname)
1129
  pobj = REAL_PROVIDERS.get(pname)
1130
  if not pobj:
1131
+ logger.info(f"[Fallback] Provider {pname} not available, skipping")
1132
  continue
1133
 
1134
  models_list = discover_provider_models(pobj, pname)
1135
  if not models_list:
1136
+ logger.warning(f"[Fallback] No models for provider {pname}")
1137
  continue
1138
 
1139
  if model_name in models_list:
 
1143
 
1144
  for m in model_candidates[:10]:
1145
  try:
1146
+ logger.info(f"[Fallback] Trying provider {pname} with model {m}")
1147
  stream = g4f.ChatCompletion.create(
1148
  model=m,
1149
  provider=pobj,
 
1165
  CACHE.set(key, full)
1166
  return
1167
  except Exception as e:
1168
+ logger.warning(
1169
+ f"[Fallback] Provider {pname} model {m} failed: {str(e)[:200]}"
1170
+ )
1171
  continue
1172
 
1173
  yield "❌ فشلت جميع المزودات. تأكد من اتصال الإنترنت أو حاول لاحقاً."
1174
 
1175
+
1176
  # =====================================================
1177
  # FASTAPI
1178
  # =====================================================
1179
+ app = FastAPI(
1180
+ title="G4F Smart Router",
1181
+ description="AI Gateway - Protocol Translator for Anthropic"
1182
+ )
1183
 
 
1184
  app.add_middleware(
1185
  CORSMiddleware,
1186
  allow_origins=["*"],
 
1191
 
1192
  API_KEY = os.getenv("API_KEY", "mysecretkey123")
1193
 
1194
+
1195
  class ChatRequest(BaseModel):
1196
  message: str
1197
  provider: str = "Blackbox"
1198
  model: str = "gpt-4o"
1199
  history: List[Any] = []
1200
 
1201
+
1202
  # =====================================================
1203
  # التحقق من المفتاح
1204
  # =====================================================
 
1221
  detail="Invalid API key. Use 'Authorization: Bearer KEY' or 'X-API-Key: KEY'"
1222
  )
1223
 
1224
+
1225
  # =====================================================
1226
+ # دعم HEAD
1227
  # =====================================================
1228
  @app.head("/")
1229
  async def head_root():
 
1245
  async def head_chat_completions():
1246
  return Response(status_code=200)
1247
 
1248
+
1249
  # =====================================================
1250
+ # نقاط نهاية متوافقة مع Anthropic API
1251
  # =====================================================
1252
  @app.get("/v1/models")
1253
  async def v1_models(request: Request):
 
1277
  }]
1278
  return {"object": "list", "data": models}
1279
 
1280
+
1281
  # =====================================================
1282
  # نقطة /v1/messages - بروتوكول Anthropic الكامل
 
1283
  # =====================================================
1284
  @app.post("/v1/messages")
1285
  async def v1_messages(request: Request):
1286
  verify_api_key(request)
1287
  body = await request.json()
1288
 
1289
+ logger.info(
1290
+ f"[API] /v1/messages - model={body.get('model')}, "
1291
+ f"stream={body.get('stream')}, "
1292
+ f"tools_count={len(body.get('tools', []))}"
1293
+ )
1294
 
1295
  messages = body.get("messages", [])
1296
  if not messages:
1297
  raise HTTPException(status_code=400, detail="No messages provided")
1298
 
 
1299
  model = body.get("model", "gpt-4o")
1300
  system_prompt = body.get("system", "")
1301
+
1302
  if isinstance(system_prompt, list):
1303
  sys_parts = []
1304
  for sp in system_prompt:
 
1312
  max_tokens = body.get("max_tokens", 4096)
1313
  tools = body.get("tools", [])
1314
  tool_choice = body.get("tool_choice", "auto")
 
 
 
 
1315
  metadata = body.get("metadata", {})
1316
 
 
1317
  full_message, history = MessageConverter.convert_messages(
1318
  messages, system_prompt, tools, tool_choice
1319
  )
1320
 
1321
+ logger.info(
1322
+ f"[API] Message length: {len(full_message)}, "
1323
+ f"history: {len(history)}, "
1324
+ f"tools: {[t.get('name', '') for t in tools]}"
1325
+ )
1326
 
 
1327
  if is_stream:
1328
  return await _handle_anthropic_stream(
1329
  full_message, history, model, max_tokens, tools, tool_choice, metadata
1330
  )
1331
 
1332
+ # Non-stream: تجميع الرد كاملاً
1333
  full_response = ""
1334
  for chunk in ask(full_message, history, "Blackbox", model):
1335
  full_response += chunk
1336
 
 
1337
  clean_text, tool_calls = ToolCallParser.parse_tool_calls(full_response, tools)
1338
 
1339
  message_id = f"msg_{int(time.time())}_{os.urandom(4).hex()}"
1340
  input_tokens = max(1, len(full_message) // 4)
1341
  output_tokens = max(1, len(full_response) // 4)
1342
 
 
1343
  content_blocks = []
1344
 
 
1345
  if clean_text.strip():
1346
  content_blocks.append({
1347
  "type": "text",
1348
  "text": clean_text.strip()
1349
  })
1350
 
1351
+ # ======================================================
1352
+ # [إصلاح #3] - إرسال tool calls مع معرفات صحيحة
1353
+ # ======================================================
1354
  for tc in tool_calls:
1355
+ tool_id = tc.get("id") or ToolCallParser.generate_tool_id()
1356
  content_blocks.append({
1357
  "type": "tool_use",
1358
+ "id": tool_id,
1359
  "name": tc["name"],
1360
  "input": tc.get("input", {})
1361
  })
1362
+ logger.info(
1363
+ f"[API] Tool call in response: {tc['name']} (id: {tool_id})"
1364
+ )
1365
 
 
1366
  if not content_blocks:
1367
  content_blocks.append({
1368
  "type": "text",
1369
  "text": full_response or ""
1370
  })
1371
 
1372
+ stop_reason = "tool_use" if tool_calls else "end_turn"
 
 
 
1373
 
1374
  return {
1375
  "id": message_id,
 
1385
  }
1386
  }
1387
 
1388
+
1389
  # =====================================================
1390
  # معالج الـ streaming بصيغة Anthropic SSE
 
1391
  # =====================================================
1392
  async def _handle_anthropic_stream(
1393
  full_message: str,
 
1419
  }
1420
  }
1421
  }
1422
+ yield (
1423
+ f"event: message_start\n"
1424
+ f"data: {json.dumps(msg_start, ensure_ascii=False)}\n\n"
1425
+ )
1426
 
 
1427
  tool_buffer = StreamToolBuffer(available_tools=tools_list)
1428
 
 
1429
  current_block_index = 0
 
1430
  text_block_open = False
1431
  output_tokens = 0
1432
  has_tool_calls = False
 
1433
 
1434
+ # ---- دوال مساعدة ----
1435
  def make_text_block_start(index: int) -> str:
1436
  block_start = {
1437
  "type": "content_block_start",
1438
  "index": index,
1439
+ "content_block": {"type": "text", "text": ""}
 
 
 
1440
  }
1441
+ return (
1442
+ f"event: content_block_start\n"
1443
+ f"data: {json.dumps(block_start, ensure_ascii=False)}\n\n"
1444
+ )
1445
 
 
1446
  def make_text_delta(index: int, text: str) -> str:
1447
  delta_event = {
1448
  "type": "content_block_delta",
1449
  "index": index,
1450
+ "delta": {"type": "text_delta", "text": text}
 
 
 
1451
  }
1452
+ return (
1453
+ f"event: content_block_delta\n"
1454
+ f"data: {json.dumps(delta_event, ensure_ascii=False)}\n\n"
1455
+ )
1456
 
 
1457
  def make_block_stop(index: int) -> str:
1458
  block_stop = {
1459
  "type": "content_block_stop",
1460
  "index": index
1461
  }
1462
+ return (
1463
+ f"event: content_block_stop\n"
1464
+ f"data: {json.dumps(block_stop, ensure_ascii=False)}\n\n"
1465
+ )
1466
 
 
1467
  def make_tool_use_events(index: int, tool_call: Dict) -> str:
1468
+ """
1469
+ إنشاء أحداث tool_use للـ stream.
1470
+ [إصلاح #3] - استخدام المعرف الصحيح من tool_call
1471
+ """
1472
  events_str = ""
1473
+ # ======================================================
1474
+ # [إصلاح #3] - ضمان وجود معرف ص��يح
1475
+ # ======================================================
1476
+ tool_id = tool_call.get("id") or ToolCallParser.generate_tool_id()
1477
  tool_name = tool_call.get("name", "unknown")
1478
  tool_input = tool_call.get("input", {})
1479
 
1480
+ logger.info(
1481
+ f"[Stream] Sending tool_use: {tool_name} (id: {tool_id})"
1482
+ )
1483
+
1484
  block_start = {
1485
  "type": "content_block_start",
1486
  "index": index,
 
1491
  "input": {}
1492
  }
1493
  }
1494
+ events_str += (
1495
+ f"event: content_block_start\n"
1496
+ f"data: {json.dumps(block_start, ensure_ascii=False)}\n\n"
1497
+ )
1498
 
1499
  # إرسال input كـ JSON delta
1500
  input_json = json.dumps(tool_input, ensure_ascii=False)
 
1501
  chunk_size = 100
1502
  for i in range(0, len(input_json), chunk_size):
1503
  json_chunk = input_json[i:i + chunk_size]
 
1509
  "partial_json": json_chunk
1510
  }
1511
  }
1512
+ events_str += (
1513
+ f"event: content_block_delta\n"
1514
+ f"data: {json.dumps(delta_event, ensure_ascii=False)}\n\n"
1515
+ )
1516
 
 
1517
  events_str += make_block_stop(index)
 
1518
  return events_str
1519
 
1520
+ # ===== معالجة الـ Stream =====
1521
  try:
1522
  for chunk in ask(full_message, history, "Blackbox", model):
1523
  if not chunk:
 
1525
 
1526
  output_tokens += max(1, len(chunk) // 4)
1527
 
 
1528
  if not tools_list:
1529
+ # لا أدوات - نرسل كنص مباشرة
1530
  if not text_block_open:
1531
  yield make_text_block_start(current_block_index)
1532
  text_block_open = True
1533
  yield make_text_delta(current_block_index, chunk)
1534
  continue
1535
 
1536
+ # هناك أدوات - نستخدم الـ buffer
1537
  events = tool_buffer.feed(chunk)
1538
 
1539
  for event in events:
 
1546
  yield make_text_delta(current_block_index, text)
1547
 
1548
  elif event.get("type") == "tool_use":
1549
+ # إغلاق كتلة النص المفتوحة
1550
  if text_block_open:
1551
  yield make_block_stop(current_block_index)
1552
  current_block_index += 1
1553
  text_block_open = False
1554
 
 
1555
  has_tool_calls = True
1556
  yield make_tool_use_events(current_block_index, event)
1557
  current_block_index += 1
 
1578
  current_block_index += 1
1579
 
1580
  except Exception as e:
1581
+ logger.error(f"[Stream] Error: {str(e)}")
 
1582
  if not text_block_open:
1583
  yield make_text_block_start(current_block_index)
1584
  text_block_open = True
 
1588
  if text_block_open:
1589
  yield make_block_stop(current_block_index)
1590
  elif current_block_index == 0 and not has_tool_calls:
 
1591
  yield make_text_block_start(0)
1592
  yield make_text_delta(0, "")
1593
  yield make_block_stop(0)
1594
 
 
1595
  stop_reason = "tool_use" if has_tool_calls else "end_turn"
1596
 
 
1597
  msg_delta = {
1598
  "type": "message_delta",
1599
  "delta": {
1600
  "stop_reason": stop_reason,
1601
  "stop_sequence": None
1602
  },
1603
+ "usage": {"output_tokens": output_tokens}
 
 
1604
  }
1605
+ yield (
1606
+ f"event: message_delta\n"
1607
+ f"data: {json.dumps(msg_delta, ensure_ascii=False)}\n\n"
1608
+ )
1609
 
1610
+ yield 'event: message_stop\ndata: {"type": "message_stop"}\n\n'
 
1611
 
1612
  return StreamingResponse(
1613
  generate_stream(),
 
1619
  }
1620
  )
1621
 
1622
+
1623
  # =====================================================
1624
+ # نقطة /v1/messages/stream
1625
  # =====================================================
1626
  @app.post("/v1/messages/stream")
1627
  async def v1_messages_stream(request: Request):
 
1647
  tools = body.get("tools", [])
1648
  tool_choice = body.get("tool_choice", "auto")
1649
 
 
1650
  full_message, history = MessageConverter.convert_messages(
1651
  messages, system_prompt, tools, tool_choice
1652
  )
1653
 
1654
+ return await _handle_anthropic_stream(
1655
+ full_message, history, model, max_tokens, tools, tool_choice
1656
+ )
1657
+
1658
 
1659
  # =====================================================
1660
  # نقطة /v1/chat/completions (صيغة OpenAI)
 
1671
  model = body.get("model", "gpt-4o")
1672
  is_stream = body.get("stream", False)
1673
 
 
1674
  last_message = messages[-1]
1675
  user_message = extract_text_from_content(last_message.get("content", ""))
1676
 
 
1677
  history = []
1678
  for msg in messages[:-1]:
1679
  role = msg.get("role", "user")
 
1684
  completion_id = f"chatcmpl-{int(time.time())}_{os.urandom(4).hex()}"
1685
 
1686
  if is_stream:
 
1687
  async def openai_stream():
1688
  for chunk in ask(user_message, history, "Blackbox", model):
1689
  if chunk:
 
1700
  }
1701
  yield f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
1702
 
 
1703
  final_data = {
1704
  "id": completion_id,
1705
  "object": "chat.completion.chunk",
 
1724
  }
1725
  )
1726
 
 
1727
  full_response = ""
1728
  for chunk in ask(user_message, history, "Blackbox", model):
1729
  full_response += chunk
 
1748
  }
1749
  }
1750
 
1751
+
1752
  # =====================================================
1753
  # نقاط نهاية إضافية
1754
  # =====================================================
 
1756
  async def root():
1757
  return {
1758
  "message": "G4F Smart Router - Protocol Translator (Anthropic Compatible)",
1759
+ "version": "2.1.0",
1760
+ "fixes_applied": [
1761
+ "[Fix #1] Selective Memory: history[:10] + history[-50:]",
1762
+ "[Fix #2] Improved Regex: non-greedy + case-insensitive + whitespace handling",
1763
+ "[Fix #3] Tool ID Binding: tool_use_id properly linked in results"
 
 
1764
  ],
1765
  "providers": list(REAL_PROVIDERS.keys()),
1766
  "endpoints": {
1767
  "GET /": "Home",
1768
  "GET /health": "Health check",
1769
+ "GET /v1/models": "List models",
1770
+ "POST /v1/messages": "Anthropic format (AUTH)",
1771
+ "POST /v1/messages/stream": "Anthropic stream (AUTH)",
1772
+ "POST /v1/chat/completions": "OpenAI format (AUTH)",
1773
  "POST /chat": "Simple chat (AUTH)",
1774
  "POST /chat/stream": "Simple stream (AUTH)",
1775
  "GET /providers": "Providers list (AUTH)",
1776
+ "GET /debug/test-tool-parse": "Test tool parsing (AUTH)",
1777
  },
1778
  "cookies": COOKIE_STATUS,
1779
  "status": "✅ Server is working"
1780
  }
1781
 
1782
+
1783
  @app.get("/health")
1784
  async def health():
1785
  return {
1786
  "status": "ok",
1787
+ "version": "2.1.0",
1788
  "cookies": COOKIE_STATUS,
1789
  "providers": list(REAL_PROVIDERS.keys()),
1790
  "provider_count": len(REAL_PROVIDERS),
1791
+ "fixes": {
1792
+ "selective_memory": True,
1793
+ "improved_regex": True,
1794
+ "tool_id_binding": True
1795
  },
1796
  "timestamp": int(time.time())
1797
  }
1798
 
1799
+
1800
  @app.get("/providers")
1801
  async def get_providers(request: Request):
1802
  verify_api_key(request)
 
1809
  }
1810
  return {"providers": result}
1811
 
1812
+
1813
  # =====================================================
1814
  # نقطة تشخيص: اختبار تحليل tool calls
1815
  # =====================================================
 
1820
  test_cases = [
1821
  {
1822
  "name": "Standard tool_call tags",
1823
+ "input": (
1824
+ 'I will create the file now.\n'
1825
+ '<tool_call>{"name": "write_file", '
1826
+ '"arguments": {"path": "test.py", "content": "print(\'hello\')"}}'
1827
+ '</tool_call>'
1828
+ ),
1829
+ },
1830
+ {
1831
+ "name": "Tool call with leading whitespace/newlines",
1832
+ "input": (
1833
+ 'Let me help you.\n\n\n'
1834
+ ' <tool_call>\n'
1835
+ '{"name": "read_file", "arguments": {"path": "config.json"}}\n'
1836
+ '</tool_call>'
1837
+ ),
1838
  },
1839
  {
1840
  "name": "Code block format",
1841
+ "input": (
1842
+ 'Let me write that.\n'
1843
+ '```tool_call\n'
1844
+ '{"name": "read_file", "arguments": {"path": "config.json"}}\n'
1845
+ '```'
1846
+ ),
1847
  },
1848
  {
1849
  "name": "Function tag format",
1850
+ "input": (
1851
+ 'Creating file:\n'
1852
+ '<function=write_file>'
1853
+ '{"path": "app.js", "content": "console.log(1)"}'
1854
+ '</function>'
1855
+ ),
1856
  },
1857
  {
1858
  "name": "Multiple tool calls",
1859
+ "input": (
1860
+ 'First read, then write.\n'
1861
+ '<tool_call>{"name": "read_file", "arguments": {"path": "old.txt"}}'
1862
+ '</tool_call>\n'
1863
+ 'Now writing:\n'
1864
+ '<tool_call>{"name": "write_file", "arguments": {"path": "new.txt", "content": "data"}}'
1865
+ '</tool_call>'
1866
+ ),
1867
+ },
1868
+ {
1869
+ "name": "Case insensitive detection",
1870
+ "input": (
1871
+ 'Using uppercase:\n'
1872
+ '<TOOL_CALL>{"name": "list_files", "arguments": {"path": "."}}'
1873
+ '</TOOL_CALL>'
1874
+ ),
1875
  },
1876
  {
1877
  "name": "No tool calls (plain text)",
 
1884
  clean_text, tool_calls = ToolCallParser.parse_tool_calls(tc["input"])
1885
  results.append({
1886
  "test_name": tc["name"],
1887
+ "input_preview": (
1888
+ tc["input"][:100] + "..."
1889
+ if len(tc["input"]) > 100
1890
+ else tc["input"]
1891
+ ),
1892
  "clean_text": clean_text,
1893
  "tool_calls_found": len(tool_calls),
1894
  "tool_calls": tool_calls,
 
1897
 
1898
  return {"test_results": results}
1899
 
1900
+
1901
  # =====================================================
1902
  # نقطة تشخيص: اختبار stream buffer
1903
  # =====================================================
 
1910
  tools = body.get("tools", [])
1911
  chunk_size = body.get("chunk_size", 10)
1912
 
 
1913
  buffer = StreamToolBuffer(available_tools=tools)
1914
  all_events = []
1915
 
 
1919
  for event in events:
1920
  all_events.append({"chunk_index": i // chunk_size, "event": event})
1921
 
 
1922
  remaining = buffer.flush()
1923
  for event in remaining:
1924
  all_events.append({"chunk_index": "flush", "event": event})
 
1931
  "events": all_events
1932
  }
1933
 
1934
+
1935
  @app.post("/chat")
1936
  async def chat(request: Request, chat_req: ChatRequest):
1937
  verify_api_key(request)
1938
  result = ""
1939
+ for chunk in ask(
1940
+ chat_req.message, chat_req.history, chat_req.provider, chat_req.model
1941
+ ):
1942
  result += chunk
1943
  return JSONResponse({"response": result})
1944
 
1945
+
1946
  @app.post("/chat/stream")
1947
  async def chat_stream(request: Request, chat_req: ChatRequest):
1948
  verify_api_key(request)
1949
 
1950
  async def generate():
1951
+ for chunk in ask(
1952
+ chat_req.message, chat_req.history, chat_req.provider, chat_req.model
1953
+ ):
1954
  yield f"data: {json.dumps({'delta': chunk}, ensure_ascii=False)}\n\n"
1955
  yield "data: [DONE]\n\n"
1956
 
 
1963
  }
1964
  )
1965
 
1966
+
1967
  # =====================================================
1968
+ # معالجة الأخطاء العامة
1969
  # =====================================================
1970
  @app.exception_handler(404)
1971
  async def not_found_handler(request: Request, exc: HTTPException):
 
1975
  "type": "error",
1976
  "error": {
1977
  "type": "not_found_error",
1978
+ "message": (
1979
+ f"Endpoint {request.method} {request.url.path} not found"
1980
+ )
1981
  }
1982
  }
1983
  )
1984
 
1985
+
1986
  @app.exception_handler(500)
1987
  async def internal_error_handler(request: Request, exc: Exception):
1988
  logger.error(f"Internal error: {str(exc)}")
 
1997
  }
1998
  )
1999
 
2000
+
2001
  @app.exception_handler(401)
2002
  async def auth_error_handler(request: Request, exc: HTTPException):
2003
  return JSONResponse(
 
2006
  "type": "error",
2007
  "error": {
2008
  "type": "authentication_error",
2009
+ "message": (
2010
+ exc.detail if hasattr(exc, 'detail') else "Invalid API key"
2011
+ )
2012
  }
2013
  }
2014
  )
2015
 
2016
+
2017
  @app.exception_handler(400)
2018
  async def bad_request_handler(request: Request, exc: HTTPException):
2019
  return JSONResponse(
 
2022
  "type": "error",
2023
  "error": {
2024
  "type": "invalid_request_error",
2025
+ "message": (
2026
+ exc.detail if hasattr(exc, 'detail') else "Bad request"
2027
+ )
2028
  }
2029
  }
2030
  )
2031
 
2032
+
2033
  @app.exception_handler(429)
2034
  async def rate_limit_handler(request: Request, exc: HTTPException):
2035
  return JSONResponse(
 
2043
  }
2044
  )
2045
 
2046
+
2047
  # =====================================================
2048
  # التشغيل
2049
  # =====================================================
2050
  if __name__ == "__main__":
2051
  import uvicorn
2052
  port = int(os.getenv("PORT", 7860))
2053
+ logger.info(f"Starting G4F Smart Router v2.1 on port {port}")
2054
  logger.info(f"Cookies: {COOKIE_STATUS}")
2055
+ logger.info(f"Providers: {list(REAL_PROVIDERS.keys())}")
2056
+ logger.info("Fixes: [Memory] [Regex] [Tool-ID-Binding]")
2057
  uvicorn.run("app:app", host="0.0.0.0", port=port, reload=False)
2058
+