ayb-bh92 commited on
Commit
aba4e31
·
verified ·
1 Parent(s): 45f4a0d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +631 -374
app.py CHANGED
@@ -4,15 +4,14 @@ import time
4
  import logging
5
  import subprocess
6
  import re
7
- from typing import Any, Dict, List
8
  from fastapi import FastAPI, HTTPException, Request
9
  from fastapi.responses import StreamingResponse, JSONResponse
10
- from fastapi.routing import APIRoute
11
  from pydantic import BaseModel
12
  import g4f
13
 
14
  logging.basicConfig(level=logging.INFO)
15
- logger = logging.getLogger("g4f-smart-router")
16
 
17
  # =====================================================
18
  # COOKIES
@@ -56,58 +55,142 @@ COOKIE_STATUS = load_cookies()
56
  # =====================================================
57
  # PROVIDERS
58
  # =====================================================
59
- def get_provider(name: str):
60
  try:
61
- return getattr(g4f.Provider, name)
62
- except:
 
63
  return None
64
 
65
- REAL_PROVIDERS: Dict[str, Any] = {
66
- "Perplexity": get_provider("Perplexity") or get_provider("PerplexityAi"),
67
- "Copilot": get_provider("Copilot"),
68
- "Qwen": get_provider("Qwen"),
69
- "Blackbox": get_provider("Blackbox"),
70
- "DeepSeek": get_provider("DeepSeek"),
71
- "HuggingFace": get_provider("HuggingFace"),
72
- "Cloudflare": get_provider("Cloudflare"),
73
- "You": get_provider("You"),
74
- "Bing": get_provider("Bing"),
75
- }
76
- REAL_PROVIDERS = {k: v for k, v in REAL_PROVIDERS.items() if v}
 
77
 
78
  # =====================================================
79
- # MODELS
80
  # =====================================================
81
- # الأسماء الصحيحة في g4f v7.5.5
82
- QWEN_WORKING_MODELS = [
83
- "qwen-3-235b",
84
- "qwen-3-32b",
85
- "qwen-3-30b",
86
- "qwen-3-14b",
87
- "qwen-3-4b",
88
- "qwen-2.5-72b",
89
- ]
 
90
 
91
- PROVIDER_MODELS_FALLBACK: Dict[str, List[str]] = {
92
- "Perplexity": ["sonar", "sonar-pro", "gpt-4o", "llama-3"],
93
- "Copilot": ["gpt-4o", "gpt-4", "turbo"],
94
- "Qwen": QWEN_WORKING_MODELS,
95
- "Blackbox": ["gpt-4o", "claude-3", "gemini-pro", "llama-3"],
96
- "DeepSeek": ["deepseek-chat", "deepseek-coder"],
97
- "HuggingFace": ["meta-llama", "mistral", "qwen", "gemma"],
98
- "Cloudflare": ["llama-3", "mistral", "qwen"],
99
- "You": ["gpt-4o", "claude", "llama-3"],
100
- "Bing": ["gpt-4o", "gpt-4"],
101
- }
102
 
103
- # كل نماذج Claude تُحوَّل إلى أفضل نموذج Qwen متاح في g4f
104
  CLAUDE_MODEL_MAP: Dict[str, str] = {
105
- "claude-opus-4-6": "qwen-3-235b",
106
- "claude-sonnet-4-6": "qwen-3-32b",
107
- "claude-haiku-4-5": "qwen-3-14b",
108
- "claude-sonnet-4-5": "qwen3.6-plus",
 
 
 
109
  }
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  # =====================================================
112
  # TOOLS
113
  # =====================================================
@@ -117,7 +200,7 @@ AVAILABLE_TOOLS = [
117
  "description": "قراءة محتوى ملف",
118
  "input_schema": {
119
  "type": "object",
120
- "properties": {"path": {"type": "string", "description": "مسار الملف"}},
121
  "required": ["path"]
122
  }
123
  },
@@ -127,8 +210,8 @@ AVAILABLE_TOOLS = [
127
  "input_schema": {
128
  "type": "object",
129
  "properties": {
130
- "path": {"type": "string", "description": "مسار الملف"},
131
- "content": {"type": "string", "description": "المحتوى"}
132
  },
133
  "required": ["path", "content"]
134
  }
@@ -138,7 +221,7 @@ AVAILABLE_TOOLS = [
138
  "description": "عرض محتوى مجلد",
139
  "input_schema": {
140
  "type": "object",
141
- "properties": {"path": {"type": "string", "description": "المسار", "default": "."}},
142
  "required": []
143
  }
144
  },
@@ -147,7 +230,7 @@ AVAILABLE_TOOLS = [
147
  "description": "تشغيل أمر shell",
148
  "input_schema": {
149
  "type": "object",
150
- "properties": {"command": {"type": "string", "description": "الأمر"}},
151
  "required": ["command"]
152
  }
153
  },
@@ -156,7 +239,7 @@ AVAILABLE_TOOLS = [
156
  "description": "حذف ملف",
157
  "input_schema": {
158
  "type": "object",
159
- "properties": {"path": {"type": "string", "description": "مسار الملف"}},
160
  "required": ["path"]
161
  }
162
  },
@@ -167,414 +250,588 @@ def execute_tool(name: str, inp: Dict[str, Any]) -> str:
167
  if name == "read_file":
168
  p = inp.get("path", "")
169
  if not os.path.exists(p):
170
- return f"❌ الملف غير موجود: {p}"
171
  with open(p, "r", encoding="utf-8", errors="replace") as f:
172
- return f.read() or "(الملف فارغ)"
173
-
174
  elif name == "write_file":
175
- p = inp.get("path", "")
176
- c = inp.get("content", "")
177
  os.makedirs(os.path.dirname(os.path.abspath(p)), exist_ok=True)
178
  with open(p, "w", encoding="utf-8") as f:
179
  f.write(c)
180
- return f"✅ تم حفظ الملف: {p}"
181
-
182
  elif name == "list_directory":
183
  p = inp.get("path", ".")
184
  if not os.path.exists(p):
185
- return f"❌ المسار غير موجود: {p}"
186
  items = sorted(os.listdir(p))
187
- lines = [("📁 " if os.path.isdir(os.path.join(p, i)) else "📄 ") + i for i in items]
188
- return "\n".join(lines) or "(فارغ)"
189
-
190
  elif name == "run_command":
191
  cmd = inp.get("command", "")
192
  r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
193
  out = (r.stdout + r.stderr).strip()
194
- return out or "(لا يوجد إخراج)"
195
-
196
  elif name == "delete_file":
197
  p = inp.get("path", "")
198
  if not os.path.exists(p):
199
- return f"❌ الملف غير موجود: {p}"
200
  os.remove(p)
201
- return f"✅ تم الحذف: {p}"
202
-
203
- return f"❌ أداة غير معروفة: {name}"
204
  except Exception as e:
205
- return f"❌ خطأ: {e}"
 
206
 
207
  # =====================================================
208
- # SYSTEM PROMPT
209
  # =====================================================
210
- SYSTEM_PROMPT = """أنت مساعد ذكي قادر على تنفيذ مهام حقيقية.
211
- عندما تحتاج لأداة اكتب بهذا الشكل فقط:
212
- <tool_use>{"name": "اسم_الأداة", "input": {"المعامل": "القيمة"}}</tool_use>
213
 
214
- الأدوات المتاحة: read_file، write_file، list_directory، run_command، delete_file
215
- نفّذ الطلب مباشرة بدون تردد."""
216
 
217
  # =====================================================
218
- # MODEL DISCOVERY
219
  # =====================================================
220
- _PROVIDER_MODEL_CACHE: Dict[str, List[str]] = {}
221
-
222
- def _normalize(x: Any) -> List[str]:
223
- if x is None: return []
224
- if isinstance(x, dict): items = list(x.keys())
225
- elif isinstance(x, (list, tuple, set)): items = list(x)
226
- else: items = [x]
227
- seen, out = set(), []
228
- for m in [str(i).strip() for i in items if i]:
229
- if m not in seen:
230
- out.append(m)
231
- seen.add(m)
232
- return out
233
 
234
- def discover_models(pobj: Any, pname: str) -> List[str]:
235
- candidates = []
236
- for attr in ("models", "model", "default_model", "available_models", "supported_models"):
237
- try:
238
- if hasattr(pobj, attr):
239
- candidates += _normalize(getattr(pobj, attr))
240
- except: pass
241
- for fn in ("get_models", "models_list", "list_models"):
242
- try:
243
- if hasattr(pobj, fn):
244
- candidates += _normalize(getattr(pobj, fn)())
245
- except: pass
246
- if not candidates:
247
- candidates = PROVIDER_MODELS_FALLBACK.get(pname, ["gpt-4o"])
248
- seen, out = set(), []
249
- for m in candidates:
250
- if m not in seen:
251
- out.append(m)
252
- seen.add(m)
253
- return out
254
-
255
- # =====================================================
256
- # CACHE
257
- # =====================================================
258
- _CACHE: Dict[str, str] = {}
259
- _CACHE_TS: Dict[str, float] = {}
260
 
261
- def cache_get(key): return _CACHE.get(key)
262
- def cache_set(key, val):
263
- _CACHE[key] = val
264
- _CACHE_TS[key] = time.time()
 
265
 
266
  # =====================================================
267
- # STREAM CLEANER
268
  # =====================================================
269
- def clean_chunk(chunk) -> str:
270
- try:
271
- if isinstance(chunk, dict):
272
- if "choices" in chunk and chunk["choices"]:
273
- delta = chunk["choices"][0].get("delta", {})
274
- return delta.get("content") or delta.get("text") or ""
275
- return chunk.get("content") or chunk.get("text") or ""
276
- if isinstance(chunk, str):
277
- if chunk.strip().startswith("{"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  try:
279
- d = json.loads(chunk)
280
- if "choices" in d and d["choices"]:
281
- return d["choices"][0].get("delta", {}).get("content", "")
282
- return d.get("content") or d.get("text") or ""
283
- except: pass
284
- return chunk.replace("\\n", "\n").replace("\\r", "\r").replace("\\t", " ")
285
- return str(chunk)
286
- except Exception as e:
287
- logger.warning(f"clean_chunk error: {e}")
288
- return ""
289
-
290
- # =====================================================
291
- # SURGICAL VALIDATION (فحص مخرجات g4f لمنع كود الأخطاء التالفة)
292
- # =====================================================
293
- def is_invalid_response(text: str) -> bool:
294
- """تحديد وحجب ردود الأخطاء الداخلية من g4f ومزوداتها قبل إرسالها للواجهة."""
295
- if not text:
296
- return True
297
- t_lower = text.lower().strip()
298
- # التحقق المباشر من الأخطاء كـ HTML span المرسلة بالصورة
299
- if "<span" in t_lower and "color" in t_lower and ("red" in t_lower or "choices" in t_lower or "nonetype" in t_lower):
300
- return True
301
- if "nonetype" in t_lower and "choices" in t_lower:
302
- return True
303
- if "object has no attribute" in t_lower:
304
- return True
305
- if "error" in t_lower and "style=" in t_lower:
306
- return True
307
- if t_lower.startswith("<!doctype html") or t_lower.startswith("<html"):
308
- return True
309
- return False
310
 
311
  # =====================================================
312
- # CORE ASK
313
  # =====================================================
314
- def ask(message: str, history: list, provider_name: str, model_name: str):
315
- message = (message or "").strip()
316
- if not message:
317
- yield ""
318
- return
319
-
320
- model_name = CLAUDE_MODEL_MAP.get(model_name, model_name)
321
- key = f"{provider_name}|{model_name}|{message}"
322
- cached = cache_get(key)
323
- if cached:
324
- yield cached
325
- return
326
-
327
- msgs = []
328
- try:
329
- if history:
330
- src = history[-40:] if isinstance(history[0], dict) else history[-20:]
331
- for item in src:
332
- if isinstance(item, dict):
333
- r, c = item.get("role"), item.get("content")
334
- if r and c:
335
- msgs.append({"role": str(r), "content": str(c)})
336
- elif isinstance(item, (list, tuple)) and len(item) == 2:
337
- u, a = item
338
- if u: msgs.append({"role": "user", "content": str(u)})
339
- if a: msgs.append({"role": "assistant", "content": str(a)})
340
- except Exception as e:
341
- logger.warning(e)
342
-
343
- msgs.append({"role": "user", "content": message})
344
-
345
- fallback_order = [provider_name, "Perplexity", "Copilot", "Blackbox", "DeepSeek", "Bing", "You", "Qwen"]
346
- used = []
347
- for pname in fallback_order:
348
- if pname in used: continue
349
- used.append(pname)
350
- pobj = REAL_PROVIDERS.get(pname)
351
- if not pobj: continue
352
- if pname not in _PROVIDER_MODEL_CACHE:
353
- _PROVIDER_MODEL_CACHE[pname] = discover_models(pobj, pname)
354
- candidates = [model_name] + [x for x in _PROVIDER_MODEL_CACHE[pname] if x != model_name]
355
- for m in candidates[:12]:
356
  try:
357
  stream = g4f.ChatCompletion.create(
358
- model=m, provider=pobj, messages=msgs, stream=True
 
 
 
359
  )
360
- text = ""
361
- has_yielded = False
362
  for chunk in stream:
363
  c = clean_chunk(chunk)
364
  if c:
365
- temp_text = text + c
366
- # الفحص الجراحي قبل الإرسال لمنع ظهور كود HTML تالف في واجهة المستخدم
367
- if is_invalid_response(temp_text) or is_invalid_response(c):
368
- logger.warning(f"Rejected invalid response from provider {pname} (model: {m})")
369
- text = ""
370
- break
371
- text = temp_text
372
- yield text
373
- has_yielded = True
374
-
375
- # فحص نهائي للرد المجمع قبل تخزينه بالذاكرة المؤقتة (cache)
376
- if text.strip() and not is_invalid_response(text):
377
- cache_set(key, text)
378
- return
379
- else:
380
- if has_yielded:
381
- yield ""
382
  continue
 
 
 
 
383
  except Exception as e:
384
- logger.warning(e)
385
  continue
386
-
387
- yield "❌ فشل مع جميع المزودين."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
388
 
389
  # =====================================================
390
  # AGENTIC LOOP
391
  # =====================================================
392
- TOOL_RE = re.compile(r"<tool_use>(.*?)</tool_use>", re.DOTALL)
393
-
394
- def agentic_ask(messages: List[Dict], tools: List[Dict] = None) -> str:
395
- system = SYSTEM_PROMPT
396
- if tools:
397
- system += f"\n\nأدوات إضافية:\n{json.dumps(tools, ensure_ascii=False)}"
398
-
399
- working = [{"role": "system", "content": system}] + list(messages)
400
- max_iter = 8
401
-
402
- for _ in range(max_iter):
403
- last_user = ""
404
- for m in reversed(working):
405
- if m.get("role") == "user":
406
- c = m.get("content", "")
407
- last_user = c if isinstance(c, str) else str(c)
408
- break
409
-
410
- history = [m for m in working[:-1]]
411
- full = ""
412
- for chunk in ask(last_user, history, "Qwen", "claude-sonnet-4-6"):
413
- full = chunk
414
-
415
- if not full.strip():
416
- return "❌ لم يأتِ رد."
417
-
418
- matches = TOOL_RE.findall(full)
419
- if not matches:
420
- return full
421
-
422
- results = []
423
- for match in matches:
424
- try:
425
- td = json.loads(match.strip())
426
- tname = td.get("name", "")
427
- tinput = td.get("input", {})
428
- logger.info(f"Tool: {tname} | Input: {tinput}")
429
- result = execute_tool(tname, tinput)
430
- results.append(f"[نتيجة {tname}]:\n{result}")
431
- except Exception as e:
432
- results.append(f"[خطأ في الأداة]: {e}")
433
-
434
- working.append({"role": "assistant", "content": full})
435
  working.append({
436
  "role": "user",
437
- "content": "نتائج الأدوات:\n" + "\n\n".join(results) + "\n\nأكمل إجابتك للمستخدم."
438
  })
439
-
440
- return full
 
441
 
442
  # =====================================================
443
  # FASTAPI
444
  # =====================================================
445
- app = FastAPI(title="G4F API")
446
  API_KEY = os.getenv("API_KEY", "mysecretkey123")
447
 
448
- class ChatRequest(BaseModel):
449
- message: str
450
- provider: str = "Qwen"
451
- model: str = "claude-sonnet-4-6"
452
- history: List[Any] = []
453
-
454
- def verify(request: Request):
455
  auth = request.headers.get("Authorization", "")
456
  if auth.startswith("Bearer ") and auth[7:] == API_KEY:
457
- return True
458
  if request.headers.get("X-API-Key", "") == API_KEY:
459
- return True
460
  raise HTTPException(status_code=401, detail="Invalid or missing API key")
461
 
 
 
 
462
  @app.api_route("/", methods=["GET", "HEAD"])
463
  async def root():
464
  return JSONResponse({
465
- "message": "G4F API is running",
466
- "model": "Qwen3 (claude-compatible)",
467
- "endpoints": {
468
- "GET /health": "فحص الخادم",
469
- "GET /v1/models": "قائمة النماذج",
470
- "POST /v1/messages": "Claude-compatible chat",
471
- "POST /chat": "chat بسيط",
472
- "POST /chat/stream": "streaming",
473
- "GET /providers": "المزودون"
474
- },
475
- "status": "✅ running"
476
  })
477
 
478
  @app.get("/health")
479
  async def health():
480
- return {"status": "ok", "cookies": COOKIE_STATUS, "providers": list(REAL_PROVIDERS.keys())}
481
-
482
- @app.post("/chat")
483
- async def chat(request: Request, req: ChatRequest):
484
- verify(request)
485
- full = ""
486
- for chunk in ask(req.message, req.history, req.provider, req.model):
487
- full = chunk
488
- return JSONResponse({"response": full})
489
-
490
- @app.post("/chat/stream")
491
- async def chat_stream(request: Request, req: ChatRequest):
492
- verify(request)
493
- async def gen():
494
- for chunk in ask(req.message, req.history, req.provider, req.model):
495
- yield f"data: {json.dumps({'chunk': chunk})}\n\n"
496
- yield "data: [DONE]\n\n"
497
- return StreamingResponse(gen(), media_type="text/event-stream")
498
 
499
- @app.get("/providers")
500
- async def get_providers(request: Request):
501
- verify(request)
502
- info = {}
503
- for pname, pobj in REAL_PROVIDERS.items():
504
- if pname not in _PROVIDER_MODEL_CACHE:
505
- _PROVIDER_MODEL_CACHE[pname] = discover_models(pobj, pname)
506
- info[pname] = _PROVIDER_MODEL_CACHE[pname]
507
- return JSONResponse({"providers": info})
 
 
 
508
 
509
  # =====================================================
510
- # Anthropic-compatible endpoints
511
  # =====================================================
512
- @app.get("/v1/models")
513
- async def list_models(request: Request):
514
- verify(request)
515
- return JSONResponse({"object": "list", "data": [
516
- {"id": "claude-opus-4-6", "object": "model", "owned_by": "anthropic"},
517
- {"id": "claude-sonnet-4-6", "object": "model", "owned_by": "anthropic"},
518
- {"id": "claude-haiku-4-5", "object": "model", "owned_by": "anthropic"},
519
- ]})
520
-
521
- @app.api_route("/v1/messages", methods=["POST"])
522
  async def messages_endpoint(request: Request):
523
- verify(request)
524
-
525
  body = await request.json()
526
- model = body.get("model", "claude-sonnet-4-6")
527
- raw_messages = body.get("messages", [])
528
- do_stream = body.get("stream", False)
529
  client_tools = body.get("tools", [])
530
-
531
- if not raw_messages:
 
532
  raise HTTPException(status_code=400, detail="messages required")
533
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
  clean_msgs = []
535
- for m in raw_messages:
 
 
 
 
 
536
  role = m.get("role", "user")
537
- content = m.get("content", "")
538
- if isinstance(content, list):
539
- text = " ".join(b.get("text", "") for b in content if isinstance(b, dict) and b.get("type") == "text")
540
- else:
541
- text = str(content)
542
- clean_msgs.append({"role": role, "content": text})
543
-
544
- all_tools = AVAILABLE_TOOLS + [t for t in client_tools if t not in AVAILABLE_TOOLS]
545
-
546
- if do_stream:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  async def stream_gen():
548
- msg_id = f"msg_{int(time.time())}"
549
- yield f"data: {json.dumps({'type':'message_start','message':{'id':msg_id,'type':'message','role':'assistant','model':model,'content':[],'stop_reason':None,'usage':{'input_tokens':0,'output_tokens':0}}})}\n\n"
550
- yield f"data: {json.dumps({'type':'content_block_start','index':0,'content_block':{'type':'text','text':''}})}\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
 
552
- response = agentic_ask(clean_msgs, all_tools)
553
- chunk_size = 30
554
- for i in range(0, len(response), chunk_size):
555
- piece = response[i:i+chunk_size]
556
- yield f"data: {json.dumps({'type':'content_block_delta','index':0,'delta':{'type':'text_delta','text':piece}})}\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
557
 
558
- yield f"data: {json.dumps({'type':'content_block_stop','index':0})}\n\n"
559
- yield f"data: {json.dumps({'type':'message_delta','delta':{'stop_reason':'end_turn','stop_sequence':None},'usage':{'output_tokens':0}})}\n\n"
560
- yield f"data: {json.dumps({'type':'message_stop'})}\n\n"
 
 
 
 
 
561
 
562
- return StreamingResponse(stream_gen(), media_type="text/event-stream")
 
 
 
 
 
 
 
 
563
 
564
- else:
565
- response = agentic_ask(clean_msgs, all_tools)
566
- return JSONResponse({
567
- "id": f"msg_{int(time.time())}",
568
- "type": "message",
569
- "role": "assistant",
570
- "model": model,
571
- "content": [{"type": "text", "text": response}],
572
- "stop_reason": "end_turn",
573
- "stop_sequence": None,
574
- "usage": {"input_tokens": 0, "output_tokens": 0}
575
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
576
 
577
  if __name__ == "__main__":
578
  import uvicorn
579
  port = int(os.getenv("PORT", "7860"))
580
- uvicorn.run(app, host="0.0.0.0", port=port)
 
4
  import logging
5
  import subprocess
6
  import re
7
+ from typing import Any, Dict, List, Optional
8
  from fastapi import FastAPI, HTTPException, Request
9
  from fastapi.responses import StreamingResponse, JSONResponse
 
10
  from pydantic import BaseModel
11
  import g4f
12
 
13
  logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger("g4f-gateway")
15
 
16
  # =====================================================
17
  # COOKIES
 
55
  # =====================================================
56
  # PROVIDERS
57
  # =====================================================
58
+ def _get_provider(name: str):
59
  try:
60
+ p = getattr(g4f.Provider, name, None)
61
+ return p if p and callable(p) else None
62
+ except Exception:
63
  return None
64
 
65
+ PRIORITY_PROVIDERS = [
66
+ "Qwen", "Blackbox", "DeepSeek", "Copilot", "Perplexity",
67
+ "PerplexityAi", "You", "HuggingFace", "Cloudflare", "Bing",
68
+ "BingChat", "BlackboxPro", "DeepInfra", "OneApi",
69
+ ]
70
+
71
+ REAL_PROVIDERS: Dict[str, Any] = {}
72
+ for pname in PRIORITY_PROVIDERS:
73
+ p = _get_provider(pname)
74
+ if p:
75
+ REAL_PROVIDERS[pname] = p
76
+
77
+ logger.info(f"Available providers: {list(REAL_PROVIDERS.keys())}")
78
 
79
  # =====================================================
80
+ # MODELS — من g4f.models
81
  # =====================================================
82
+ def _discover_models() -> Dict[str, str]:
83
+ models = {}
84
+ try:
85
+ for attr in dir(g4f.models):
86
+ val = getattr(g4f.models, attr)
87
+ if isinstance(val, str) and not attr.startswith('_') and len(val) > 1:
88
+ models[attr] = val
89
+ except Exception as e:
90
+ logger.warning(f"Failed to discover g4f models: {e}")
91
+ return models
92
 
93
+ G4F_MODELS = _discover_models()
94
+
95
+ def _find_model(patterns: list) -> str:
96
+ for pat in patterns:
97
+ for key, val in G4F_MODELS.items():
98
+ if pat.lower() == val.lower():
99
+ return val
100
+ for key, val in G4F_MODELS.items():
101
+ if pat.lower().replace('-', '_') == key.lower():
102
+ return val
103
+ return patterns[0]
104
 
 
105
  CLAUDE_MODEL_MAP: Dict[str, str] = {
106
+ "claude-opus-4-6": _find_model(["qwen-3-235b", "qwen3-235b", "qwen-3-235b-a22b"]),
107
+ "claude-opus-4-5": _find_model(["qwen-3-235b", "qwen3-235b"]),
108
+ "claude-sonnet-4-6": _find_model(["qwen-3-32b", "qwen3-32b", "qwen-plus"]),
109
+ "claude-sonnet-4-5": _find_model(["qwen-3-32b", "qwen3-32b"]),
110
+ "claude-haiku-4-6": _find_model(["qwen-3-14b", "qwen3-14b", "qwen-turbo"]),
111
+ "claude-haiku-4-5": _find_model(["qwen-3-14b", "qwen3-14b"]),
112
+ "claude-haiku-4-5-20251001": _find_model(["qwen-3-14b", "qwen3-14b"]),
113
  }
114
 
115
+ # =====================================================
116
+ # ERROR FILTER — كشف الردود الخاطئة
117
+ # =====================================================
118
+ _ERROR_PATTERNS = [
119
+ re.compile(r"<html", re.IGNORECASE),
120
+ re.compile(r"<!DOCTYPE", re.IGNORECASE),
121
+ re.compile(r"<span style", re.IGNORECASE),
122
+ re.compile(r"Model not found", re.IGNORECASE),
123
+ re.compile(r"<[^>]+>'NoneType'", re.IGNORECASE),
124
+ re.compile(r"403\s+Forbidden", re.IGNORECASE),
125
+ re.compile(r"429\s+Too Many Requests", re.IGNORECASE),
126
+ re.compile(r"502\s+Bad Gateway", re.IGNORECASE),
127
+ re.compile(r"cloudflare", re.IGNORECASE),
128
+ re.compile(r"captcha", re.IGNORECASE),
129
+ re.compile(r"['\"]code['\"]\s*:\s*['\"](Forbidden|Error|Not_Found)['\"]", re.IGNORECASE),
130
+ re.compile(r"success.*[Ff]alse", re.IGNORECASE),
131
+ ]
132
+
133
+ def is_invalid_response(text: str) -> bool:
134
+ if not text or len(text.strip()) < 5:
135
+ return True
136
+ lower = text.strip().lower()
137
+ # JSON error check
138
+ if lower.startswith('{'):
139
+ try:
140
+ d = json.loads(text)
141
+ if isinstance(d, dict):
142
+ data = d.get("data", d)
143
+ if isinstance(data, dict):
144
+ code = data.get("code") or data.get("details", "").strip()
145
+ if code in ("Not_Found", "Forbidden", "Error"):
146
+ return True
147
+ if isinstance(d.get("success"), bool) and not d["success"]:
148
+ return True
149
+ if any(k in d.lower() for k in ("error", "failed", "denied")):
150
+ return True
151
+ except Exception:
152
+ pass
153
+ for pat in _ERROR_PATTERNS:
154
+ if pat.search(text):
155
+ return True
156
+ return False
157
+
158
+ # =====================================================
159
+ # STRIP THINKING
160
+ # =====================================================
161
+ def strip_thinking(text: str) -> str:
162
+ text = re.sub(r'', '', text, flags=re.DOTALL)
163
+ text = re.sub(r'<thinking>.*?</thinking>', '', text, flags=re.DOTALL)
164
+ # trailing thinking block
165
+ text = re.sub(r'[\n\r]*$', '', text, flags=re.MULTILINE)
166
+ text = text.strip()
167
+ if text == "":
168
+ return "⚠️ Provider returned an empty response"
169
+ return text
170
+
171
+ # =====================================================
172
+ # CLEAN CHUNK
173
+ # =====================================================
174
+ def clean_chunk(chunk: Any) -> str:
175
+ if chunk is None:
176
+ return ""
177
+ if isinstance(chunk, str):
178
+ return chunk
179
+ if isinstance(chunk, dict):
180
+ if "choices" in chunk and chunk.get("choices"):
181
+ Ch = chunk["choices"]
182
+ if isinstance(Ch, list) and len(Ch) > 0:
183
+ delta = Ch[0].get("delta", {})
184
+ content = delta.get("content") or delta.get("text") or ""
185
+ return content if isinstance(content, str) else ""
186
+ if "content" in chunk:
187
+ c = chunk["content"]
188
+ return str(c) if isinstance(c, str) else ""
189
+ if "text" in chunk:
190
+ t = chunk["text"]
191
+ return str(t) if isinstance(t, str) else ""
192
+ return str(chunk)
193
+
194
  # =====================================================
195
  # TOOLS
196
  # =====================================================
 
200
  "description": "قراءة محتوى ملف",
201
  "input_schema": {
202
  "type": "object",
203
+ "properties": {"path": {"type": "string"}},
204
  "required": ["path"]
205
  }
206
  },
 
210
  "input_schema": {
211
  "type": "object",
212
  "properties": {
213
+ "path": {"type": "string"},
214
+ "content": {"type": "string"}
215
  },
216
  "required": ["path", "content"]
217
  }
 
221
  "description": "عرض محتوى مجلد",
222
  "input_schema": {
223
  "type": "object",
224
+ "properties": {"path": {"type": "string", "default": "."}},
225
  "required": []
226
  }
227
  },
 
230
  "description": "تشغيل أمر shell",
231
  "input_schema": {
232
  "type": "object",
233
+ "properties": {"command": {"type": "string"}},
234
  "required": ["command"]
235
  }
236
  },
 
239
  "description": "حذف ملف",
240
  "input_schema": {
241
  "type": "object",
242
+ "properties": {"path": {"type": "string"}},
243
  "required": ["path"]
244
  }
245
  },
 
250
  if name == "read_file":
251
  p = inp.get("path", "")
252
  if not os.path.exists(p):
253
+ return f"Error: file not found: {p}"
254
  with open(p, "r", encoding="utf-8", errors="replace") as f:
255
+ return f.read() or "(empty file)"
 
256
  elif name == "write_file":
257
+ p, c = inp.get("path", ""), inp.get("content", "")
 
258
  os.makedirs(os.path.dirname(os.path.abspath(p)), exist_ok=True)
259
  with open(p, "w", encoding="utf-8") as f:
260
  f.write(c)
261
+ return f"Written {len(c)} bytes to {p}"
 
262
  elif name == "list_directory":
263
  p = inp.get("path", ".")
264
  if not os.path.exists(p):
265
+ return f"Error: path not found: {p}"
266
  items = sorted(os.listdir(p))
267
+ lines = [("dir " if os.path.isdir(os.path.join(p, i)) else "file ") + i for i in items]
268
+ return "\n".join(lines) or "(empty)"
 
269
  elif name == "run_command":
270
  cmd = inp.get("command", "")
271
  r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
272
  out = (r.stdout + r.stderr).strip()
273
+ return out or "(no output)"
 
274
  elif name == "delete_file":
275
  p = inp.get("path", "")
276
  if not os.path.exists(p):
277
+ return f"Error: file not found: {p}"
278
  os.remove(p)
279
+ return f"Deleted: {p}"
 
 
280
  except Exception as e:
281
+ return f"Tool error: {e}"
282
+ return f"Unknown tool: {name}"
283
 
284
  # =====================================================
285
+ # SYSTEM PROMPT — بتنسيق tool_use صحيح
286
  # =====================================================
287
+ SYSTEM_PROMPT = """You are a helpful AI assistant with access to tools.
288
+ When you need to use a tool, respond with ONLY this JSON (no other text before or after):
289
+ {"tool": "tool_name", "input": {"param": "value"}}
290
 
291
+ Available tools: read_file, write_file, list_directory, run_command, delete_file.
292
+ After receiving tool results, provide your final answer to the user."""
293
 
294
  # =====================================================
295
+ # ASYNC EXECUTOR
296
  # =====================================================
297
+ def _get_executor():
298
+ import concurrent.futures
299
+ return concurrent.futures.ThreadPoolExecutor(max_workers=8)
 
 
 
 
 
 
 
 
 
 
300
 
301
+ _executor_cache = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
 
303
+ def get_executor():
304
+ global _executor_cache
305
+ if _executor_cache is None:
306
+ _executor_cache = _get_executor()
307
+ return _executor_cache
308
 
309
  # =====================================================
310
+ # G4F CALL
311
  # =====================================================
312
+ async def g4f_call(messages: List[Dict], model: str, provider: str = None) -> str:
313
+ loop = asyncio.get_event_loop()
314
+ pool = get_executor()
315
+
316
+ async def _call_sync():
317
+ # تحديد النموذج النهائي
318
+ target_model = CLAUDE_MODEL_MAP.get(model, model)
319
+ providers_to_try = []
320
+
321
+ # الترتيب الأولوي: المزود المطلوب أو الافتراضي + فلاتر
322
+ if provider and provider in REAL_PROVIDERS:
323
+ providers_to_try.append((provider, target_model))
324
+ # إضافة مزودات أخرى في نفس النوع
325
+ base_name = provider.split('.')[0] if '.' in provider else provider
326
+ for pname in REAL_PROVIDERS:
327
+ if pname == provider or pname.startswith(base_name):
328
+ providers_to_try.append((pname, target_model))
329
+ else:
330
+ # وضع غامض
331
+ for pname in REAL_PROVIDERS.keys():
332
+ providers_to_try.append((pname, target_model))
333
+
334
+ seen = set()
335
+ for pobj, pm in providers_to_try:
336
+ key = f"{pobj.__name__}_{pm}"
337
+ if key in seen:
338
+ continue
339
+ seen.add(key)
340
+
341
+ # محاولة النموذج نفسه أولاً
342
+ try:
343
+ result = g4f.ChatCompletion.create(
344
+ model=pm,
345
+ provider=pobj,
346
+ messages=messages,
347
+ stream=False,
348
+ )
349
+ text = str(result or "")
350
+ if is_invalid_response(text):
351
+ continue
352
+ text = strip_thinking(text)
353
+ if text:
354
+ logger.info(f"Provider success: {pobj.__name__} model={pm}")
355
+ return text
356
+ except Exception as e:
357
+ logger.debug(f"Provider failed: {pobj.__name__} model={pm}: {e}")
358
+ continue
359
+
360
+ # الفشل — المحاولين بـ fallback models
361
+ for pobj, pm in providers_to_try:
362
+ for fallback in ["gpt-4o", "gpt-4", "claude-3-haiku", "gpt-3.5-turbo"]:
363
+ key = f"{pobj.__name__}_{fallback}"
364
+ if key in seen:
365
+ continue
366
+ seen.add(key)
367
  try:
368
+ result = g4f.ChatCompletion.create(
369
+ model=fallback,
370
+ provider=pobj,
371
+ messages=messages,
372
+ stream=False,
373
+ )
374
+ text = str(result or "")
375
+ if is_invalid_response(text):
376
+ continue
377
+ text = strip_thinking(text)
378
+ if text:
379
+ logger.info(f"Provider fallback success: {pobj.__name__} model={fallback}")
380
+ return text
381
+ except Exception as e:
382
+ logger.debug(f"Provider fallback failed: {pobj.__name__} model={fallback}: {e}")
383
+ continue
384
+
385
+ raise RuntimeError("All providers failed after trying fallback models too")
386
+
387
+ return await loop.run_in_executor(pool, _call_sync)
 
 
 
 
 
 
 
 
 
 
 
388
 
389
  # =====================================================
390
+ # STREAMING G4F
391
  # =====================================================
392
+ async def g4f_stream(messages: List[Dict], model: str, provider: str = None) -> str:
393
+ loop = asyncio.get_event_loop()
394
+ pool = get_executor()
395
+
396
+ async def _stream_sync():
397
+ target_model = CLAUDE_MODEL_MAP.get(model, model)
398
+ providers_to_try = []
399
+
400
+ if provider and provider in REAL_PROVIDERS:
401
+ providers_to_try.append((provider, target_model))
402
+ base_name = provider.split('.')[0] if '.' in provider else provider
403
+ for pname in REAL_PROVIDERS:
404
+ if pname == provider or pname.startswith(base_name):
405
+ providers_to_try.append((pname, target_model))
406
+ else:
407
+ for pname in REAL_PROVIDERS.keys():
408
+ providers_to_try.append((pname, target_model))
409
+
410
+ seen = set()
411
+ for pobj, pm in providers_to_try:
412
+ key = f"{pobj.__name__}_{pm}"
413
+ if key in seen:
414
+ continue
415
+ seen.add(key)
416
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  try:
418
  stream = g4f.ChatCompletion.create(
419
+ model=pm,
420
+ provider=pobj,
421
+ messages=messages,
422
+ stream=True,
423
  )
424
+ buffer = ""
 
425
  for chunk in stream:
426
  c = clean_chunk(chunk)
427
  if c:
428
+ buffer += c
429
+ if is_invalid_response(buffer):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  continue
431
+ text = strip_thinking(buffer)
432
+ if text:
433
+ logger.info(f"Streaming success: {pobj.__name__} model={pm}")
434
+ return text
435
  except Exception as e:
436
+ logger.debug(f"Streaming failed: {pobj.__name__} model={pm}: {e}")
437
  continue
438
+
439
+ # fallback models
440
+ for pobj, _ in providers_to_try:
441
+ for fallback in ["gpt-4o", "gpt-4", "claude-3-haiku", "gpt-3.5-turbo"]:
442
+ key = f"{pobj.__name__}_{fallback}"
443
+ if key in seen:
444
+ continue
445
+ seen.add(key)
446
+ try:
447
+ stream = g4f.ChatCompletion.create(
448
+ model=fallback,
449
+ provider=pobj,
450
+ messages=messages,
451
+ stream=True,
452
+ )
453
+ buffer = ""
454
+ for chunk in stream:
455
+ c = clean_chunk(chunk)
456
+ if c:
457
+ buffer += c
458
+ if is_invalid_response(buffer):
459
+ continue
460
+ text = strip_thinking(buffer)
461
+ if text:
462
+ logger.info(f"Streaming fallback success: {pobj.__name__} model={fallback}")
463
+ return text
464
+ except Exception as e:
465
+ logger.debug(f"Streaming fallback failed: {pobj.__name__} model={fallback}: {e}")
466
+ continue
467
+
468
+ raise RuntimeError("All provider streams failed after fallbacks")
469
+
470
+ return await loop.run_in_executor(pool, _stream_sync)
471
 
472
  # =====================================================
473
  # AGENTIC LOOP
474
  # =====================================================
475
+ async def agentic_loop(messages: List[Dict], tools: List[Dict], model: str) -> tuple:
476
+ working = list(messages)
477
+ tool_calls_log = []
478
+
479
+ if not any(m.get("role") == "system" for m in working):
480
+ working = [{"role": "system", "content": SYSTEM_PROMPT}] + working
481
+
482
+ for iteration in range(6):
483
+ text = await g4f_call(working, model)
484
+
485
+ # JSON request pattern
486
+ json_match = re.search(r'\{\s*"tool"\s*:\s*"([^"]+)"', text, re.DOTALL)
487
+ if not json_match:
488
+ logger.info(f"Iter {iteration}: No tool request found → final answer")
489
+ return text, tool_calls_log
490
+
491
+ try:
492
+ tool_req = json.loads(json_match.group(0))
493
+ tname = tool_req.get("tool", "")
494
+ tinput = tool_req.get("input", {})
495
+ except json.JSONDecodeError as e:
496
+ logger.debug(f"Iter {iteration}: invalid JSON: {e}")
497
+ return text, tool_calls_log
498
+
499
+ logger.info(f"[iter={iteration}] tool={tname}")
500
+ loop = asyncio.get_event_loop()
501
+ result = await loop.run_in_executor(get_executor(), execute_tool, tname, tinput)
502
+
503
+ tool_id = f"toolu_{iteration:04d}"
504
+ tool_calls_log.append({
505
+ "id": tool_id,
506
+ "name": tname,
507
+ "input": tinput,
508
+ "result": result
509
+ })
510
+
511
+ working.append({"role": "assistant", "content": text})
 
 
 
 
 
 
512
  working.append({
513
  "role": "user",
514
+ "content": f"Tool result for {tname}:\n{result}\n\nNow provide your final answer."
515
  })
516
+
517
+ logger.warning(f"Iteration exhausted: {iteration}")
518
+ return text, tool_calls_log
519
 
520
  # =====================================================
521
  # FASTAPI
522
  # =====================================================
523
+ app = FastAPI(title="G4F Anthropic-compatible Gateway")
524
  API_KEY = os.getenv("API_KEY", "mysecretkey123")
525
 
526
+ def _verify(request: Request):
 
 
 
 
 
 
527
  auth = request.headers.get("Authorization", "")
528
  if auth.startswith("Bearer ") and auth[7:] == API_KEY:
529
+ return
530
  if request.headers.get("X-API-Key", "") == API_KEY:
531
+ return
532
  raise HTTPException(status_code=401, detail="Invalid or missing API key")
533
 
534
+ # =====================================================
535
+ # ROOT & HEALTH
536
+ # =====================================================
537
  @app.api_route("/", methods=["GET", "HEAD"])
538
  async def root():
539
  return JSONResponse({
540
+ "status": "running",
541
+ "service": "G4F Anthropic compatible gateway",
542
+ "cookies": COOKIE_STATUS,
543
+ "providers": len(REAL_PROVIDERS),
544
+ "version": "7.5.6"
 
 
 
 
 
 
545
  })
546
 
547
  @app.get("/health")
548
  async def health():
549
+ return {
550
+ "status": "ok",
551
+ "cookies": COOKIE_STATUS,
552
+ "providers": list(REAL_PROVIDERS.keys())
553
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
554
 
555
+ @app.get("/v1/models")
556
+ async def list_models(request: Request):
557
+ _verify(request)
558
+ return JSONResponse({
559
+ "object": "list",
560
+ "data": [
561
+ {"id": "claude-opus-4-6", "object": "model", "created": 1710000000, "owned_by": "g4f"},
562
+ {"id": "claude-sonnet-4-6", "object": "model", "created": 1710000000, "owned_by": "g4f"},
563
+ {"id": "claude-haiku-4-6", "object": "model", "created": 1710000000, "owned_by": "g4f"},
564
+ {"id": "claude-haiku-4-5", "object": "model", "created": 1710000000, "owned_by": "g4f"},
565
+ ]
566
+ })
567
 
568
  # =====================================================
569
+ # /v1/messages
570
  # =====================================================
571
+ @app.post("/v1/messages")
 
 
 
 
 
 
 
 
 
572
  async def messages_endpoint(request: Request):
573
+ _verify(request)
 
574
  body = await request.json()
575
+ model = body.get("model", "claude-sonnet-4-6")
576
+ raw_msgs = body.get("messages", [])
577
+ do_stream = body.get("stream", False)
578
  client_tools = body.get("tools", [])
579
+ system_body = body.get("system", "")
580
+
581
+ if not raw_msgs:
582
  raise HTTPException(status_code=400, detail="messages required")
583
+
584
+ # تنظيف الرسائل
585
+ def _content_to_str(content):
586
+ if isinstance(content, str):
587
+ return content
588
+ if isinstance(content, list):
589
+ parts = []
590
+ for block in content:
591
+ if not isinstance(block, dict):
592
+ parts.append(str(block))
593
+ continue
594
+ btype = block.get("type", "")
595
+ if btype == "text":
596
+ parts.append(block.get("text", ""))
597
+ elif btype == "tool_result":
598
+ rc = block.get("content", "")
599
+ if isinstance(rc, list):
600
+ rc = "\n".join(
601
+ b.get("text", "") if isinstance(b, dict) else str(b)
602
+ for b in rc
603
+ )
604
+ parts.append(f"[Tool result]: {rc}")
605
+ elif btype == "tool_use":
606
+ pass
607
+ else:
608
+ parts.append(str(block.get("text", block.get("content", ""))))
609
+ return "\n".join(p for p in parts if p)
610
+ return str(content)
611
+
612
  clean_msgs = []
613
+ if system_body:
614
+ txt = _content_to_str(system_body)
615
+ if txt:
616
+ clean_msgs.append({"role": "system", "content": txt})
617
+
618
+ for m in raw_msgs:
619
  role = m.get("role", "user")
620
+ if role == "tool":
621
+ role = "user"
622
+ text = _content_to_str(m.get("content", ""))
623
+ if text:
624
+ clean_msgs.append({"role": role, "content": text})
625
+
626
+ all_tools = AVAILABLE_TOOLS + [
627
+ t for t in client_tools
628
+ if t.get("name") not in {bt["name"] for bt in AVAILABLE_TOOLS}
629
+ ]
630
+
631
+ if not do_stream:
632
+ final_text, tlog = await agentic_loop(clean_msgs, all_tools, model)
633
+
634
+ content_blocks = []
635
+ for tc in tlog:
636
+ content_blocks.append({
637
+ "type": "tool_use",
638
+ "id": tc["id"],
639
+ "name": tc["name"],
640
+ "input": tc["input"]
641
+ })
642
+
643
+ final_text = re.sub(r'$', '', final_text, flags=re.MULTILINE).strip()
644
+ if final_text:
645
+ content_blocks.append({"type": "text", "text": final_text})
646
+
647
+ if not content_blocks:
648
+ content_blocks.append({"type": "text", "text": final_text})
649
+
650
+ stop_reason = "tool_use" if tlog else "end_turn"
651
+
652
+ return JSONResponse({
653
+ "id": f"msg_{int(time.time()*1000)}",
654
+ "type": "message",
655
+ "role": "assistant",
656
+ "model": model,
657
+ "content": content_blocks,
658
+ "stop_reason": stop_reason,
659
+ "stop_sequence": None,
660
+ "usage": {"input_tokens": 0, "output_tokens": len(final_text)}
661
+ })
662
+
663
+ else:
664
  async def stream_gen():
665
+ msg_id = f"msg_{int(time.time()*1000)}"
666
+
667
+ yield "event: message_start\ndata: " + json.dumps({
668
+ "type": "message_start",
669
+ "message": {
670
+ "id": msg_id, "type": "message", "role": "assistant",
671
+ "model": model, "content": [],
672
+ "stop_reason": None, "stop_sequence": None,
673
+ "usage": {"input_tokens": 0, "output_tokens": 0}
674
+ }
675
+ }) + "\n\n"
676
+ yield 'event: ping\ndata: {"type":"ping"}\n\n'
677
+
678
+ try:
679
+ final_text, tlog = await agentic_loop(clean_msgs, all_tools, model)
680
+ except Exception as e:
681
+ logger.exception("agentic_loop error")
682
+ final_text, tlog = f"Error: {e}", []
683
+
684
+ block_idx = 0
685
+
686
+ # tool_use blocks
687
+ for tc in tlog:
688
+ tc_json = json.dumps(tc["input"])
689
+ yield "event: content_block_start\ndata: " + json.dumps({
690
+ "type": "content_block_start",
691
+ "index": block_idx,
692
+ "content_block": {
693
+ "type": "tool_use",
694
+ "id": tc["id"],
695
+ "name": tc["name"],
696
+ "input": {}
697
+ }
698
+ }) + "\n\n"
699
+ yield "event: content_block_delta\ndata: " + json.dumps({
700
+ "type": "content_block_delta",
701
+ "index": block_idx,
702
+ "delta": {"type": "input_json_delta", "partial_json": tc_json}
703
+ }) + "\n\n"
704
+ yield "event: content_block_stop\ndata: " + json.dumps({
705
+ "type": "content_block_stop", "index": block_idx
706
+ }) + "\n\n"
707
+ block_idx += 1
708
+
709
+ # text block
710
+ final_text = re.sub(r'$', '', final_text, flags=re.MULTILINE).strip()
711
+ output_text = final_text if final_text else ""
712
+
713
+ if output_text:
714
+ yield "event: content_block_start\ndata: " + json.dumps({
715
+ "type": "content_block_start",
716
+ "index": block_idx,
717
+ "content_block": {"type": "text", "text": ""}
718
+ }) + "\n\n"
719
+ chunk_size = 20
720
+ for i in range(0, len(output_text), chunk_size):
721
+ piece = output_text[i:i+chunk_size]
722
+ yield "event: content_block_delta\ndata: " + json.dumps({
723
+ "type": "content_block_delta",
724
+ "index": block_idx,
725
+ "delta": {"type": "text_delta", "text": piece}
726
+ }) + "\n\n"
727
+ await asyncio.sleep(0)
728
+ yield "event: content_block_stop\ndata: " + json.dumps({
729
+ "type": "content_block_stop", "index": block_idx
730
+ }) + "\n\n"
731
+
732
+ stop_reason = "tool_use" if tlog else "end_turn"
733
+ yield "event: message_delta\ndata: " + json.dumps({
734
+ "type": "message_delta",
735
+ "delta": {"stop_reason": stop_reason, "stop_sequence": None},
736
+ "usage": {"output_tokens": len(output_text)}
737
+ }) + "\n\n"
738
+ yield 'event: message_stop\ndata: {"type":"message_stop"}\n\n'
739
+
740
+ return StreamingResponse(stream_gen(), media_type="text/event-stream", headers={
741
+ "Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"
742
+ })
743
 
744
+ # =====================================================
745
+ # MCP
746
+ # =====================================================
747
+ @app.post("/mcp")
748
+ async def mcp_endpoint(request: Request):
749
+ _verify(request)
750
+ body = await request.json()
751
+ method = body.get("method", "")
752
+ req_id = body.get("id", 1)
753
+
754
+ if method == "tools/list":
755
+ return JSONResponse({
756
+ "jsonrpc": "2.0", "id": req_id,
757
+ "result": {"tools": [
758
+ {"name": t["name"], "description": t["description"], "inputSchema": t["input_schema"]}
759
+ for t in AVAILABLE_TOOLS
760
+ ]}
761
+ })
762
+
763
+ elif method == "tools/call":
764
+ params = body.get("params", {})
765
+ tname = params.get("name", "")
766
+ tinput = params.get("arguments", {})
767
+ loop = asyncio.get_event_loop()
768
+ result = await loop.run_in_executor(get_executor(), execute_tool, tname, tinput)
769
+ return JSONResponse({
770
+ "jsonrpc": "2.0", "id": req_id,
771
+ "result": {"content": [{"type": "text", "text": result}]}
772
+ })
773
+
774
+ elif method in ("initialize", "notifications/initialized"):
775
+ return JSONResponse({
776
+ "jsonrpc": "2.0", "id": req_id,
777
+ "result": {
778
+ "protocolVersion": "2024-11-05",
779
+ "capabilities": {"tools": {}},
780
+ "serverInfo": {"name": "g4f-gateway", "version": "1.0.0"}
781
+ }
782
+ })
783
+
784
+ return JSONResponse({
785
+ "jsonrpc": "2.0", "id": req_id,
786
+ "error": {"code": -32601, "message": f"Method not found: {method}"}
787
+ })
788
 
789
+ # =====================================================
790
+ # SIMPLE CHAT
791
+ # =====================================================
792
+ class ChatRequest(BaseModel):
793
+ message: str
794
+ provider: str = "Qwen"
795
+ model: str = "claude-sonnet-4-6"
796
+ history: List[Any] = []
797
 
798
+ @app.post("/chat")
799
+ async def chat(request: Request, req: ChatRequest):
800
+ _verify(request)
801
+ msgs = [{"role": "user", "content": req.message}]
802
+ try:
803
+ text = await g4f_call(msgs, req.model, req.provider)
804
+ except Exception as e:
805
+ text = f"Error: {e}"
806
+ return JSONResponse({"response": text})
807
 
808
+ @app.post("/chat/stream")
809
+ async def chat_stream(request: Request, req: ChatRequest):
810
+ _verify(request)
811
+ async def gen():
812
+ msgs = [{"role": "user", "content": req.message}]
813
+ try:
814
+ text = await g4f_stream(msgs, req.model, req.provider)
815
+ except Exception as e:
816
+ text = f"Error: {e}"
817
+ chunk_size = 20
818
+ for i in range(0, len(text), chunk_size):
819
+ yield f"data: {json.dumps({'chunk': text[i:i+chunk_size]})}\n\n"
820
+ await asyncio.sleep(0)
821
+ yield "data: [DONE]\n\n"
822
+ return StreamingResponse(gen(), media_type="text/event-stream")
823
+
824
+ @app.get("/providers")
825
+ async def get_providers(request: Request):
826
+ _verify(request)
827
+ return JSONResponse({
828
+ "providers": {
829
+ pname: CLAUDE_MODEL_MAP.get(pname, pname) if pname in REAL_PROVIDERS else []
830
+ for pname in REAL_PROVIDERS
831
+ }
832
+ })
833
 
834
  if __name__ == "__main__":
835
  import uvicorn
836
  port = int(os.getenv("PORT", "7860"))
837
+ uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")