ayb-bh92 commited on
Commit
214fe14
·
verified ·
1 Parent(s): 9bb914e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1580 -142
app.py CHANGED
@@ -5,6 +5,7 @@ import logging
5
  import subprocess
6
  import re
7
  import asyncio
 
8
  from concurrent.futures import ThreadPoolExecutor
9
  from typing import Any, Dict, List, Optional
10
 
@@ -13,233 +14,638 @@ from fastapi import FastAPI, HTTPException, Request
13
  from fastapi.responses import StreamingResponse, JSONResponse
14
  from pydantic import BaseModel
15
 
16
- logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
 
 
 
 
 
17
  logger = logging.getLogger("g4f-smart-router")
18
 
 
19
  # =====================================================
20
  # THREAD POOL — لتجنب تجميد Event Loop
21
  # =====================================================
 
22
  _POOL = ThreadPoolExecutor(max_workers=8)
23
 
 
24
  # =====================================================
25
  # COOKIES
26
  # =====================================================
 
27
  def _load_cookies_raw() -> Dict[str, Any]:
28
  raw_env = (os.getenv("COOKIES_JSON") or "").strip()
 
29
  if raw_env:
30
  try:
31
  return json.loads(raw_env)
32
  except Exception as e:
33
  logger.warning(e)
 
34
  try:
35
  if os.path.exists("cookies.json"):
36
  with open("cookies.json", "r", encoding="utf-8") as f:
37
  return json.load(f)
 
38
  except Exception as e:
39
  logger.warning(e)
 
40
  return {}
41
 
 
42
  def load_cookies() -> str:
43
  data = _load_cookies_raw()
 
44
  if not data:
45
  return "⚠️ No Cookies"
 
46
  try:
47
  from g4f.cookies import set_cookies
 
48
  except Exception:
49
  return "⚠️ Cookies Found"
 
50
  for domain, vals in data.items():
51
  try:
52
  dom = domain if "." in domain else f".{domain}.com"
 
53
  if isinstance(vals, list):
54
- vals = {x["name"]: x["value"] for x in vals if isinstance(x, dict)}
 
 
 
 
 
 
 
55
  if isinstance(vals, dict):
56
  set_cookies(dom, vals)
 
57
  except Exception as e:
58
  logger.warning(e)
 
59
  return "✅ Cookies Loaded"
60
 
 
61
  COOKIE_STATUS = load_cookies()
62
 
 
63
  # =====================================================
64
  # PROVIDERS (Qwen Only)
65
  # =====================================================
 
66
  def get_provider(name: str):
67
  try:
68
  return getattr(g4f.Provider, name)
69
- except:
70
  return None
71
 
 
72
  REAL_PROVIDERS = {
73
  "Qwen": get_provider("Qwen"),
74
  }
75
- REAL_PROVIDERS = {k: v for k, v in REAL_PROVIDERS.items() if v}
 
 
 
 
 
 
76
 
77
  # =====================================================
78
- # MODELS - تحديث شامل بناء على بحث الانترنت لأسماء Qwen
79
  # =====================================================
 
80
  QWEN_WORKING_MODELS = [
81
- "qwen-max", # الاسم الرسمي لأقوى نسخة
82
- "qwen-max-latest", # تحديثات الداش سكوب
83
- "qwen3.7-max", # تسميات محتملة
84
  "qwen2.5-max",
85
- "qwen2.5-72b-instruct", # التسمية مفتوحة المصدر الشائعة
86
  "qwen-plus",
87
  "qwen-turbo",
88
- "" # بعض المزودين يستخدم النموذج الافتراضي إذا كان فارغاً
89
  ]
90
 
 
91
  PROVIDER_MODELS_FALLBACK: Dict[str, List[str]] = {
92
  "Qwen": QWEN_WORKING_MODELS,
93
  }
94
 
 
95
  CLAUDE_MODEL_MAP = {
96
- "claude-opus-4-6": "qwen-max",
97
  "claude-sonnet-4-6": "qwen-max",
98
- "claude-haiku-4-5": "qwen-plus",
99
  }
100
 
 
101
  # =====================================================
102
  # CACHE
103
  # =====================================================
 
104
  _CACHE: Dict[str, str] = {}
105
  _CACHE_TS: Dict[str, float] = {}
106
 
 
107
  def cache_get(key):
108
  ts = _CACHE_TS.get(key)
 
109
  if ts and (time.time() - ts) < 300:
110
  return _CACHE.get(key)
 
111
  return None
112
 
 
113
  def cache_set(key, val):
114
  _CACHE[key] = val
115
  _CACHE_TS[key] = time.time()
116
 
 
117
  # =====================================================
118
  # STREAM CLEANER
119
  # =====================================================
 
120
  def _clean_chunk(chunk) -> str:
121
  try:
122
  if isinstance(chunk, dict):
 
123
  if "choices" in chunk and chunk["choices"]:
124
- delta = chunk["choices"][0].get("delta", {})
125
- return delta.get("content") or delta.get("text") or ""
126
- return chunk.get("content") or chunk.get("text") or ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  if isinstance(chunk, str):
 
128
  if chunk.strip().startswith("{"):
129
  try:
130
  d = json.loads(chunk)
 
131
  if d.get("code") == "Not_Found":
132
  return ""
 
133
  if "choices" in d and d["choices"]:
134
- return d["choices"][0].get("delta", {}).get("content", "")
135
- return d.get("content") or d.get("text") or ""
136
- except:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  pass
 
138
  return chunk.replace("\\n", "\n")
 
139
  return str(chunk)
140
- except Exception as e:
 
141
  return ""
142
 
 
143
  # =====================================================
144
- # CORE ASK (Sync Generator)
145
  # =====================================================
146
- def ask(message: str, history: list, provider_name: str, model_name: str):
 
 
 
 
 
 
147
  message = (message or "").strip()
 
148
  if not message and not history:
149
  yield ""
150
  return
151
 
152
- target_model = CLAUDE_MODEL_MAP.get(model_name, model_name)
153
- key = f"{provider_name}|{target_model}|{hash(json.dumps(message) + json.dumps(history[-3:] if history else []))}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  cached = cache_get(key)
 
155
  if cached:
156
  yield cached
157
  return
158
 
159
  msgs = []
 
160
  if history:
161
  for item in history[-20:]:
 
162
  if isinstance(item, dict):
163
- r, c = item.get("role"), item.get("content")
164
- if r and c and str(c).strip():
165
- msgs.append({"role": str(r), "content": str(c)})
 
 
 
 
 
 
 
 
 
166
 
167
  if message:
168
- msgs.append({"role": "user", "content": message})
 
 
 
169
 
170
  fallback_order = ["Qwen"]
 
171
  last_exc = None
172
 
173
  for pname in fallback_order:
 
174
  pobj = REAL_PROVIDERS.get(pname)
 
175
  if not pobj:
176
  continue
177
 
178
- candidates = [target_model] + [x for x in QWEN_WORKING_MODELS if x != target_model]
 
 
 
 
 
 
 
 
 
179
 
180
- for m in candidates:
181
  try:
182
  stream = g4f.ChatCompletion.create(
183
- model=m,
184
  provider=pobj,
185
  messages=msgs,
186
  stream=True,
187
  )
 
188
  text = ""
 
189
  for chunk in stream:
190
- c = _clean_chunk(chunk)
191
- if c:
192
- text += c
 
193
 
194
  if text.strip():
195
  cache_set(key, text)
 
196
  yield text
197
  return
 
198
  except Exception as e:
199
  err_str = str(e)
200
- # اخفاء أخطاء عدم توفر النموذج في اللوج لتجنب الإزعاج والانتقال للنموذج التالي بسلاسة
201
- if "Not_Found" in err_str or "Model not found" in err_str:
 
 
 
202
  continue
 
203
  last_exc = e
204
  continue
205
 
206
- err_msg = f"❌ فشل الاتصال بمزود Qwen نهائياً. الخطأ الأخير: {last_exc}"
 
 
 
 
207
  logger.error(err_msg)
 
208
  yield err_msg
209
 
 
210
  # =====================================================
211
- # AGENTIC ASK - تم الجراحة هنا (تمرير الأدوات للمستخدم وليس تنفيذها)
212
  # =====================================================
 
213
  def _simple_ask_sync(messages: List[Dict]) -> str:
214
  last_user = ""
215
  history = []
216
-
217
- # فصل رسالة المستخدم الأخيرة عن التاريخ
218
- for i, m in enumerate(reversed(messages)):
219
- if m.get("role") == "user":
220
- last_user = m.get("content", "")
221
- history = messages[:-i-1] if i != 0 else messages[:-1]
 
 
 
 
 
 
222
  break
223
-
224
  if not last_user and messages:
225
  history = messages
226
  last_user = "Please proceed."
227
 
228
  full = ""
229
- for chunk in ask(last_user, history, "Qwen", "claude-sonnet-4-6"):
 
 
 
 
 
 
230
  full = chunk
231
 
232
  return full.strip()
233
 
234
- async def ask_llm_async(messages: List[Dict]) -> str:
 
 
 
235
  loop = asyncio.get_running_loop()
236
- return await loop.run_in_executor(_POOL, _simple_ask_sync, messages)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
 
238
  # =====================================================
239
  # FASTAPI
240
  # =====================================================
241
- app = FastAPI(title="G4F API")
242
- API_KEY = os.getenv("API_KEY", "mysecretkey123")
 
 
 
 
 
 
 
 
 
243
 
244
  class ChatRequest(BaseModel):
245
  message: str
@@ -247,142 +653,1174 @@ class ChatRequest(BaseModel):
247
  model: str = "claude-sonnet-4-6"
248
  history: List[Any] = []
249
 
 
250
  def verify(request: Request):
251
- auth = request.headers.get("Authorization", "")
252
- if auth.startswith("Bearer ") and auth[7:] == API_KEY:
253
- return True
254
- if request.headers.get("X-API-Key", "") == API_KEY:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255
  return True
256
- raise HTTPException(status_code=401, detail="Invalid or missing API key")
257
 
258
- @app.api_route("/", methods=["GET", "HEAD"])
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  async def root():
260
- return JSONResponse({"status": "✅ running", "model": "Qwen Proxy for Claude"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
 
262
  @app.get("/v1/models")
263
  async def list_models(request: Request):
264
  verify(request)
265
- return JSONResponse({"object": "list", "data": [
266
- {"id": "claude-opus-4-6", "object": "model", "owned_by": "anthropic"},
267
- {"id": "claude-sonnet-4-6", "object": "model", "owned_by": "anthropic"},
268
- {"id": "claude-haiku-4-5", "object": "model", "owned_by": "anthropic"},
269
- ]})
270
 
271
- @app.api_route("/v1/messages", methods=["POST"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  async def messages_endpoint(request: Request):
273
  verify(request)
274
 
275
  body = await request.json()
276
- model = body.get("model", "claude-sonnet-4-6")
277
- raw_messages = body.get("messages", [])
278
- do_stream = body.get("stream", False)
279
- client_tools = body.get("tools", [])
280
- system_req = body.get("system", "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
 
282
  if not raw_messages:
283
- raise HTTPException(status_code=400, detail="messages required")
 
 
 
 
 
 
 
 
 
284
 
285
- # إضافة تعليمات الأدوات القادمة من Claude Code إلى Qwen ليقوم بصياغتها بطريقة نستطيع التقاطها
286
- system_prompt = system_req if isinstance(system_req, str) else str(system_req)
287
  if client_tools:
288
- tools_str = json.dumps(client_tools, ensure_ascii=False)
289
- system_prompt += f"\n\nYou have access to tools. To use one, reply ONLY with this XML format:\n<tool_call>{{\"name\": \"tool_name\", \"input\": {{\"param\": \"value\"}}}}</tool_call>\nAvailable tools: {tools_str}"
 
 
 
 
 
 
 
 
 
 
 
 
 
290
 
291
  clean_msgs = []
 
292
  if system_prompt.strip():
293
- clean_msgs.append({"role": "system", "content": system_prompt})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
 
295
- for m in raw_messages:
296
- role = m.get("role", "user")
297
- content = m.get("content", "")
298
  if isinstance(content, list):
299
- text = " ".join(b.get("text", "") for b in content if isinstance(b, dict) and b.get("type") == "text")
300
- # استخراج نتائج الأدوات لو أرسلها Claude Code
301
- for b in content:
302
- if isinstance(b, dict) and b.get("type") == "tool_result":
303
- text += f"\nTool Result [{b.get('tool_use_id')}]: {b.get('content')}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  else:
305
  text = str(content)
306
- clean_msgs.append({"role": role, "content": text})
 
 
 
 
 
 
 
 
307
 
308
  if do_stream:
 
309
  async def stream_gen():
310
- msg_id = f"msg_{int(time.time())}"
311
- 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"
312
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  try:
314
- response_text = await ask_llm_async(clean_msgs)
 
 
 
 
 
315
  except Exception as e:
316
  response_text = f"Error: {e}"
317
 
318
- # البحث عن أمر أداة بداخل نص رد كوين
319
- tool_match = re.search(r"<tool_call>(.*?)</tool_call>", response_text, re.DOTALL)
320
-
321
- if tool_match:
322
- # إذا طلب كوين استخدام أداة، نقوم بتجهيزها لـ Claude Code
323
- tool_json_str = tool_match.group(1).strip()
324
- pre_text = response_text[:tool_match.start()].strip()
325
-
326
- try:
327
- tool_data = json.loads(tool_json_str)
328
- t_name = tool_data.get("name", "unknown")
329
- t_input = tool_data.get("input", {})
330
- t_id = f"toolu_{int(time.time())}"
331
-
332
- idx = 0
333
- if pre_text:
334
- yield f"data: {json.dumps({'type':'content_block_start','index':idx,'content_block':{'type':'text','text':''}})}\n\n"
335
- chunk_size = 30
336
- for i in range(0, len(pre_text), chunk_size):
337
- piece = pre_text[i:i+chunk_size]
338
- yield f"data: {json.dumps({'type':'content_block_delta','index':idx,'delta':{'type':'text_delta','text':piece}})}\n\n"
339
- yield f"data: {json.dumps({'type':'content_block_stop','index':idx})}\n\n"
340
- idx += 1
341
-
342
- # إرسال أوامر الأداة لـ Claude Code ليقوم بتنفيذها على جهازك
343
- yield f"data: {json.dumps({'type':'content_block_start','index':idx,'content_block':{'type':'tool_use','id':t_id,'name':t_name,'input':{}}})}\n\n"
344
- yield f"data: {json.dumps({'type':'content_block_delta','index':idx,'delta':{'type':'input_json_delta','partial_json':json.dumps(t_input)}})}\n\n"
345
- yield f"data: {json.dumps({'type':'content_block_stop','index':idx})}\n\n"
346
-
347
- yield f"data: {json.dumps({'type':'message_delta','delta':{'stop_reason':'tool_use','stop_sequence':None},'usage':{'output_tokens':10}})}\n\n"
348
- yield f"data: {json.dumps({'type':'message_stop'})}\n\n"
349
- return
350
 
351
- except Exception as e:
352
- logger.error(f"Failed to parse tool JSON: {e}")
353
- pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
 
355
- # الرد العادي بدون أدوات
356
- yield f"data: {json.dumps({'type':'content_block_start','index':0,'content_block':{'type':'text','text':''}})}\n\n"
357
  chunk_size = 30
358
- for i in range(0, len(response_text), chunk_size):
359
- piece = response_text[i:i+chunk_size]
360
- yield f"data: {json.dumps({'type':'content_block_delta','index':0,'delta':{'type':'text_delta','text':piece}})}\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  await asyncio.sleep(0.005)
362
 
363
- yield f"data: {json.dumps({'type':'content_block_stop','index':0})}\n\n"
364
- yield f"data: {json.dumps({'type':'message_delta','delta':{'stop_reason':'end_turn','stop_sequence':None},'usage':{'output_tokens':len(response_text)//4}})}\n\n"
365
- yield f"data: {json.dumps({'type':'message_stop'})}\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
 
367
  return StreamingResponse(
368
  stream_gen(),
369
  media_type="text/event-stream",
370
- headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}
 
 
 
 
371
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  else:
373
- # مسار Non-stream مشابه يمكن تركه كـ fallback
374
- response_text = await ask_llm_async(clean_msgs)
375
- return JSONResponse({
376
- "id": f"msg_{int(time.time())}",
377
- "type": "message",
378
- "role": "assistant",
379
- "model": model,
380
- "content": [{"type": "text", "text": response_text}],
381
- "stop_reason": "end_turn",
382
- "usage": {"input_tokens": 0, "output_tokens": len(response_text)//4}
383
  })
384
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
  if __name__ == "__main__":
386
  import uvicorn
387
- port = int(os.getenv("PORT", "7860"))
388
- uvicorn.run(app, host="0.0.0.0", port=port)
 
 
 
 
 
 
 
 
 
 
 
 
5
  import subprocess
6
  import re
7
  import asyncio
8
+ import uuid
9
  from concurrent.futures import ThreadPoolExecutor
10
  from typing import Any, Dict, List, Optional
11
 
 
14
  from fastapi.responses import StreamingResponse, JSONResponse
15
  from pydantic import BaseModel
16
 
17
+
18
+ logging.basicConfig(
19
+ level=logging.INFO,
20
+ format="%(asctime)s [%(levelname)s] %(message)s"
21
+ )
22
+
23
  logger = logging.getLogger("g4f-smart-router")
24
 
25
+
26
  # =====================================================
27
  # THREAD POOL — لتجنب تجميد Event Loop
28
  # =====================================================
29
+
30
  _POOL = ThreadPoolExecutor(max_workers=8)
31
 
32
+
33
  # =====================================================
34
  # COOKIES
35
  # =====================================================
36
+
37
  def _load_cookies_raw() -> Dict[str, Any]:
38
  raw_env = (os.getenv("COOKIES_JSON") or "").strip()
39
+
40
  if raw_env:
41
  try:
42
  return json.loads(raw_env)
43
  except Exception as e:
44
  logger.warning(e)
45
+
46
  try:
47
  if os.path.exists("cookies.json"):
48
  with open("cookies.json", "r", encoding="utf-8") as f:
49
  return json.load(f)
50
+
51
  except Exception as e:
52
  logger.warning(e)
53
+
54
  return {}
55
 
56
+
57
  def load_cookies() -> str:
58
  data = _load_cookies_raw()
59
+
60
  if not data:
61
  return "⚠️ No Cookies"
62
+
63
  try:
64
  from g4f.cookies import set_cookies
65
+
66
  except Exception:
67
  return "⚠️ Cookies Found"
68
+
69
  for domain, vals in data.items():
70
  try:
71
  dom = domain if "." in domain else f".{domain}.com"
72
+
73
  if isinstance(vals, list):
74
+ vals = {
75
+ x["name"]: x["value"]
76
+ for x in vals
77
+ if isinstance(x, dict)
78
+ and "name" in x
79
+ and "value" in x
80
+ }
81
+
82
  if isinstance(vals, dict):
83
  set_cookies(dom, vals)
84
+
85
  except Exception as e:
86
  logger.warning(e)
87
+
88
  return "✅ Cookies Loaded"
89
 
90
+
91
  COOKIE_STATUS = load_cookies()
92
 
93
+
94
  # =====================================================
95
  # PROVIDERS (Qwen Only)
96
  # =====================================================
97
+
98
  def get_provider(name: str):
99
  try:
100
  return getattr(g4f.Provider, name)
101
+ except Exception:
102
  return None
103
 
104
+
105
  REAL_PROVIDERS = {
106
  "Qwen": get_provider("Qwen"),
107
  }
108
+
109
+ REAL_PROVIDERS = {
110
+ k: v
111
+ for k, v in REAL_PROVIDERS.items()
112
+ if v
113
+ }
114
+
115
 
116
  # =====================================================
117
+ # MODELS
118
  # =====================================================
119
+
120
  QWEN_WORKING_MODELS = [
121
+ "qwen-max",
122
+ "qwen-max-latest",
123
+ "qwen3.7-max",
124
  "qwen2.5-max",
125
+ "qwen2.5-72b-instruct",
126
  "qwen-plus",
127
  "qwen-turbo",
128
+ "",
129
  ]
130
 
131
+
132
  PROVIDER_MODELS_FALLBACK: Dict[str, List[str]] = {
133
  "Qwen": QWEN_WORKING_MODELS,
134
  }
135
 
136
+
137
  CLAUDE_MODEL_MAP = {
138
+ "claude-opus-4-6": "qwen-max",
139
  "claude-sonnet-4-6": "qwen-max",
140
+ "claude-haiku-4-5": "qwen-plus",
141
  }
142
 
143
+
144
  # =====================================================
145
  # CACHE
146
  # =====================================================
147
+
148
  _CACHE: Dict[str, str] = {}
149
  _CACHE_TS: Dict[str, float] = {}
150
 
151
+
152
  def cache_get(key):
153
  ts = _CACHE_TS.get(key)
154
+
155
  if ts and (time.time() - ts) < 300:
156
  return _CACHE.get(key)
157
+
158
  return None
159
 
160
+
161
  def cache_set(key, val):
162
  _CACHE[key] = val
163
  _CACHE_TS[key] = time.time()
164
 
165
+
166
  # =====================================================
167
  # STREAM CLEANER
168
  # =====================================================
169
+
170
  def _clean_chunk(chunk) -> str:
171
  try:
172
  if isinstance(chunk, dict):
173
+
174
  if "choices" in chunk and chunk["choices"]:
175
+ choice = chunk["choices"][0]
176
+
177
+ delta = choice.get("delta", {}) or {}
178
+ message = choice.get("message", {}) or {}
179
+
180
+ return (
181
+ delta.get("content")
182
+ or delta.get("text")
183
+ or message.get("content")
184
+ or ""
185
+ )
186
+
187
+ return (
188
+ chunk.get("content")
189
+ or chunk.get("text")
190
+ or ""
191
+ )
192
+
193
  if isinstance(chunk, str):
194
+
195
  if chunk.strip().startswith("{"):
196
  try:
197
  d = json.loads(chunk)
198
+
199
  if d.get("code") == "Not_Found":
200
  return ""
201
+
202
  if "choices" in d and d["choices"]:
203
+ choice = d["choices"][0]
204
+
205
+ delta = choice.get("delta", {}) or {}
206
+ message = choice.get("message", {}) or {}
207
+
208
+ return (
209
+ delta.get("content")
210
+ or message.get("content")
211
+ or ""
212
+ )
213
+
214
+ return (
215
+ d.get("content")
216
+ or d.get("text")
217
+ or ""
218
+ )
219
+
220
+ except Exception:
221
  pass
222
+
223
  return chunk.replace("\\n", "\n")
224
+
225
  return str(chunk)
226
+
227
+ except Exception:
228
  return ""
229
 
230
+
231
  # =====================================================
232
+ # CORE ASK (Sync Generator)
233
  # =====================================================
234
+
235
+ def ask(
236
+ message: str,
237
+ history: list,
238
+ provider_name: str,
239
+ model_name: str,
240
+ ):
241
  message = (message or "").strip()
242
+
243
  if not message and not history:
244
  yield ""
245
  return
246
 
247
+ target_model = CLAUDE_MODEL_MAP.get(
248
+ model_name,
249
+ model_name,
250
+ )
251
+
252
+ cache_data = {
253
+ "provider": provider_name,
254
+ "model": target_model,
255
+ "message": message,
256
+ "history": history[-3:] if history else [],
257
+ }
258
+
259
+ key = json.dumps(
260
+ cache_data,
261
+ ensure_ascii=False,
262
+ sort_keys=True,
263
+ default=str,
264
+ )
265
+
266
  cached = cache_get(key)
267
+
268
  if cached:
269
  yield cached
270
  return
271
 
272
  msgs = []
273
+
274
  if history:
275
  for item in history[-20:]:
276
+
277
  if isinstance(item, dict):
278
+ role = item.get("role")
279
+ content = item.get("content")
280
+
281
+ if (
282
+ role
283
+ and content
284
+ and str(content).strip()
285
+ ):
286
+ msgs.append({
287
+ "role": str(role),
288
+ "content": str(content),
289
+ })
290
 
291
  if message:
292
+ msgs.append({
293
+ "role": "user",
294
+ "content": message,
295
+ })
296
 
297
  fallback_order = ["Qwen"]
298
+
299
  last_exc = None
300
 
301
  for pname in fallback_order:
302
+
303
  pobj = REAL_PROVIDERS.get(pname)
304
+
305
  if not pobj:
306
  continue
307
 
308
+ candidates = (
309
+ [target_model]
310
+ + [
311
+ x
312
+ for x in QWEN_WORKING_MODELS
313
+ if x != target_model
314
+ ]
315
+ )
316
+
317
+ for model_candidate in candidates:
318
 
 
319
  try:
320
  stream = g4f.ChatCompletion.create(
321
+ model=model_candidate,
322
  provider=pobj,
323
  messages=msgs,
324
  stream=True,
325
  )
326
+
327
  text = ""
328
+
329
  for chunk in stream:
330
+ cleaned = _clean_chunk(chunk)
331
+
332
+ if cleaned:
333
+ text += cleaned
334
 
335
  if text.strip():
336
  cache_set(key, text)
337
+
338
  yield text
339
  return
340
+
341
  except Exception as e:
342
  err_str = str(e)
343
+
344
+ if (
345
+ "Not_Found" in err_str
346
+ or "Model not found" in err_str
347
+ ):
348
  continue
349
+
350
  last_exc = e
351
  continue
352
 
353
+ err_msg = (
354
+ "❌ فشل الاتصال بمزود Qwen نهائياً. "
355
+ f"الخطأ الأخير: {last_exc}"
356
+ )
357
+
358
  logger.error(err_msg)
359
+
360
  yield err_msg
361
 
362
+
363
  # =====================================================
364
+ # AGENTIC ASK
365
  # =====================================================
366
+
367
  def _simple_ask_sync(messages: List[Dict]) -> str:
368
  last_user = ""
369
  history = []
370
+
371
+ for i, message in enumerate(reversed(messages)):
372
+
373
+ if message.get("role") == "user":
374
+ last_user = message.get("content", "")
375
+
376
+ history = (
377
+ messages[:-i - 1]
378
+ if i != 0
379
+ else messages[:-1]
380
+ )
381
+
382
  break
383
+
384
  if not last_user and messages:
385
  history = messages
386
  last_user = "Please proceed."
387
 
388
  full = ""
389
+
390
+ for chunk in ask(
391
+ last_user,
392
+ history,
393
+ "Qwen",
394
+ "claude-sonnet-4-6",
395
+ ):
396
  full = chunk
397
 
398
  return full.strip()
399
 
400
+
401
+ async def ask_llm_async(
402
+ messages: List[Dict],
403
+ ) -> str:
404
  loop = asyncio.get_running_loop()
405
+
406
+ return await loop.run_in_executor(
407
+ _POOL,
408
+ _simple_ask_sync,
409
+ messages,
410
+ )
411
+
412
+
413
+ # =====================================================
414
+ # TOOL HELPERS
415
+ # =====================================================
416
+
417
+ def build_tools_prompt(tools: List[Dict]) -> str:
418
+ """
419
+ تحويل أدوات OpenAI إلى تعليمات نصية لـ Qwen.
420
+ """
421
+
422
+ if not tools:
423
+ return ""
424
+
425
+ tools_str = json.dumps(
426
+ tools,
427
+ ensure_ascii=False,
428
+ )
429
+
430
+ return (
431
+ "\n\nYou have access to tools.\n"
432
+ "If you need to use a tool, reply ONLY with:\n"
433
+ '<tool_call>{"name":"tool_name","input":{"param":"value"}}</tool_call>\n'
434
+ "Do not use markdown code fences.\n"
435
+ "Do not explain the tool call.\n"
436
+ f"Available tools:\n{tools_str}"
437
+ )
438
+
439
+
440
+ def extract_tool_call(
441
+ response_text: str,
442
+ ) -> Optional[Dict[str, Any]]:
443
+ """
444
+ استخراج tool_call من رد Qwen.
445
+ """
446
+
447
+ match = re.search(
448
+ r"<tool_call>\s*(.*?)\s*</tool_call>",
449
+ response_text,
450
+ re.DOTALL,
451
+ )
452
+
453
+ if not match:
454
+ return None
455
+
456
+ try:
457
+ tool_data = json.loads(
458
+ match.group(1).strip()
459
+ )
460
+
461
+ name = tool_data.get("name")
462
+
463
+ arguments = (
464
+ tool_data.get("input")
465
+ or tool_data.get("arguments")
466
+ or {}
467
+ )
468
+
469
+ if not name:
470
+ return None
471
+
472
+ return {
473
+ "name": str(name),
474
+ "arguments": arguments,
475
+ "start": match.start(),
476
+ "end": match.end(),
477
+ }
478
+
479
+ except Exception as e:
480
+ logger.error(
481
+ f"Failed to parse tool JSON: {e}"
482
+ )
483
+
484
+ return None
485
+
486
+
487
+ # =====================================================
488
+ # OPENAI HELPERS
489
+ # =====================================================
490
+
491
+ def openai_error(
492
+ message: str,
493
+ error_type: str = "invalid_request_error",
494
+ code: Optional[str] = None,
495
+ ):
496
+ return {
497
+ "error": {
498
+ "message": message,
499
+ "type": error_type,
500
+ "param": None,
501
+ "code": code,
502
+ }
503
+ }
504
+
505
+
506
+ def normalize_openai_content(content: Any) -> str:
507
+ """
508
+ تحويل content بصيغ OpenAI المختلفة إلى نص.
509
+ """
510
+
511
+ if content is None:
512
+ return ""
513
+
514
+ if isinstance(content, str):
515
+ return content
516
+
517
+ if isinstance(content, list):
518
+ parts = []
519
+
520
+ for block in content:
521
+
522
+ if not isinstance(block, dict):
523
+ parts.append(str(block))
524
+ continue
525
+
526
+ block_type = block.get("type")
527
+
528
+ if block_type in (
529
+ "text",
530
+ "input_text",
531
+ "output_text",
532
+ ):
533
+ parts.append(
534
+ str(
535
+ block.get("text")
536
+ or block.get("content")
537
+ or ""
538
+ )
539
+ )
540
+
541
+ elif block_type == "tool_result":
542
+ parts.append(
543
+ "Tool Result "
544
+ f"[{block.get('tool_use_id', '')}]: "
545
+ f"{block.get('content', '')}"
546
+ )
547
+
548
+ else:
549
+ text = (
550
+ block.get("text")
551
+ or block.get("content")
552
+ )
553
+
554
+ if text:
555
+ parts.append(str(text))
556
+
557
+ return "\n".join(parts)
558
+
559
+ return str(content)
560
+
561
+
562
+ def normalize_openai_messages(
563
+ raw_messages: List[Dict],
564
+ ) -> List[Dict]:
565
+ clean_messages = []
566
+
567
+ for message in raw_messages:
568
+
569
+ if not isinstance(message, dict):
570
+ continue
571
+
572
+ role = str(
573
+ message.get("role", "user")
574
+ )
575
+
576
+ content = normalize_openai_content(
577
+ message.get("content", "")
578
+ )
579
+
580
+ if role == "assistant":
581
+
582
+ tool_calls = message.get("tool_calls")
583
+
584
+ if tool_calls:
585
+ tool_texts = []
586
+
587
+ for tool_call in tool_calls:
588
+
589
+ function_data = (
590
+ tool_call.get("function", {})
591
+ )
592
+
593
+ tool_texts.append(
594
+ "Assistant requested tool "
595
+ f"{function_data.get('name', '')} "
596
+ f"with arguments "
597
+ f"{function_data.get('arguments', '{}')}"
598
+ )
599
+
600
+ if tool_texts:
601
+ content += (
602
+ "\n"
603
+ + "\n".join(tool_texts)
604
+ )
605
+
606
+ elif role == "tool":
607
+
608
+ tool_call_id = message.get(
609
+ "tool_call_id",
610
+ "",
611
+ )
612
+
613
+ content = (
614
+ f"Tool Result [{tool_call_id}]: "
615
+ f"{content}"
616
+ )
617
+
618
+ role = "user"
619
+
620
+ clean_messages.append({
621
+ "role": role,
622
+ "content": content,
623
+ })
624
+
625
+ return clean_messages
626
+
627
+
628
+ def approximate_tokens(text: str) -> int:
629
+ if not text:
630
+ return 0
631
+
632
+ return max(1, len(text) // 4)
633
+
634
 
635
  # =====================================================
636
  # FASTAPI
637
  # =====================================================
638
+
639
+ app = FastAPI(
640
+ title="G4F API"
641
+ )
642
+
643
+
644
+ API_KEY = os.getenv(
645
+ "API_KEY",
646
+ "mysecretkey123",
647
+ )
648
+
649
 
650
  class ChatRequest(BaseModel):
651
  message: str
 
653
  model: str = "claude-sonnet-4-6"
654
  history: List[Any] = []
655
 
656
+
657
  def verify(request: Request):
658
+ """
659
+ Authentication:
660
+
661
+ Authorization: Bearer API_KEY
662
+
663
+ ويبقى X-API-Key للتوافق مع الكود السابق.
664
+ """
665
+
666
+ auth = request.headers.get(
667
+ "Authorization",
668
+ "",
669
+ )
670
+
671
+ if auth.startswith("Bearer "):
672
+
673
+ token = auth[7:].strip()
674
+
675
+ if token == API_KEY:
676
+ return True
677
+
678
+ if (
679
+ request.headers.get("X-API-Key", "")
680
+ == API_KEY
681
+ ):
682
  return True
 
683
 
684
+ raise HTTPException(
685
+ status_code=401,
686
+ detail="Invalid or missing API key",
687
+ )
688
+
689
+
690
+ # =====================================================
691
+ # ROOT
692
+ # =====================================================
693
+
694
+ @app.api_route(
695
+ "/",
696
+ methods=["GET", "HEAD"],
697
+ )
698
  async def root():
699
+ return JSONResponse({
700
+ "status": "✅ running",
701
+ "model": "Qwen Proxy",
702
+ "openai_base_url": "/v1",
703
+ "openai_chat_completions": (
704
+ "/v1/chat/completions"
705
+ ),
706
+ "anthropic_messages": "/v1/messages",
707
+ "cookie_status": COOKIE_STATUS,
708
+ })
709
+
710
+
711
+ # =====================================================
712
+ # OPENAI MODELS
713
+ # =====================================================
714
 
715
  @app.get("/v1/models")
716
  async def list_models(request: Request):
717
  verify(request)
 
 
 
 
 
718
 
719
+ created = int(time.time())
720
+
721
+ return JSONResponse({
722
+ "object": "list",
723
+ "data": [
724
+ {
725
+ "id": "claude-opus-4-6",
726
+ "object": "model",
727
+ "created": created,
728
+ "owned_by": "anthropic",
729
+ },
730
+ {
731
+ "id": "claude-sonnet-4-6",
732
+ "object": "model",
733
+ "created": created,
734
+ "owned_by": "anthropic",
735
+ },
736
+ {
737
+ "id": "claude-haiku-4-5",
738
+ "object": "model",
739
+ "created": created,
740
+ "owned_by": "anthropic",
741
+ },
742
+ ],
743
+ })
744
+
745
+
746
+ # =====================================================
747
+ # OPENAI CHAT COMPLETIONS
748
+ # =====================================================
749
+
750
+ @app.post("/v1/chat/completions")
751
+ async def openai_chat_completions(
752
+ request: Request,
753
+ ):
754
+ verify(request)
755
+
756
+ try:
757
+ body = await request.json()
758
+
759
+ except Exception:
760
+ return JSONResponse(
761
+ status_code=400,
762
+ content=openai_error(
763
+ "Invalid JSON body"
764
+ ),
765
+ )
766
+
767
+ model = body.get(
768
+ "model",
769
+ "claude-sonnet-4-6",
770
+ )
771
+
772
+ raw_messages = body.get(
773
+ "messages",
774
+ [],
775
+ )
776
+
777
+ do_stream = bool(
778
+ body.get("stream", False)
779
+ )
780
+
781
+ client_tools = (
782
+ body.get("tools")
783
+ or []
784
+ )
785
+
786
+ tool_choice = body.get(
787
+ "tool_choice",
788
+ "auto",
789
+ )
790
+
791
+ stream_options = (
792
+ body.get("stream_options")
793
+ or {}
794
+ )
795
+
796
+ if not isinstance(raw_messages, list):
797
+ return JSONResponse(
798
+ status_code=400,
799
+ content=openai_error(
800
+ "messages must be an array"
801
+ ),
802
+ )
803
+
804
+ if not raw_messages:
805
+ return JSONResponse(
806
+ status_code=400,
807
+ content=openai_error(
808
+ "messages required"
809
+ ),
810
+ )
811
+
812
+ clean_messages = normalize_openai_messages(
813
+ raw_messages
814
+ )
815
+
816
+ if client_tools and tool_choice != "none":
817
+
818
+ tools_prompt = build_tools_prompt(
819
+ client_tools
820
+ )
821
+
822
+ if (
823
+ clean_messages
824
+ and clean_messages[0].get("role")
825
+ == "system"
826
+ ):
827
+ clean_messages[0]["content"] += (
828
+ tools_prompt
829
+ )
830
+
831
+ else:
832
+ clean_messages.insert(
833
+ 0,
834
+ {
835
+ "role": "system",
836
+ "content": tools_prompt.strip(),
837
+ },
838
+ )
839
+
840
+ completion_id = (
841
+ f"chatcmpl-{uuid.uuid4().hex}"
842
+ )
843
+
844
+ created = int(time.time())
845
+
846
+ # =================================================
847
+ # OPENAI STREAM
848
+ # =================================================
849
+
850
+ if do_stream:
851
+
852
+ async def stream_gen():
853
+
854
+ try:
855
+ response_text = await ask_llm_async(
856
+ clean_messages
857
+ )
858
+
859
+ except Exception as e:
860
+ logger.exception(
861
+ "OpenAI stream upstream error"
862
+ )
863
+
864
+ error_event = openai_error(
865
+ str(e),
866
+ "upstream_error",
867
+ )
868
+
869
+ yield (
870
+ "data: "
871
+ + json.dumps(
872
+ error_event,
873
+ ensure_ascii=False,
874
+ )
875
+ + "\n\n"
876
+ )
877
+
878
+ yield "data: [DONE]\n\n"
879
+
880
+ return
881
+
882
+ tool_data = extract_tool_call(
883
+ response_text
884
+ )
885
+
886
+ prompt_text = json.dumps(
887
+ clean_messages,
888
+ ensure_ascii=False,
889
+ )
890
+
891
+ prompt_tokens = approximate_tokens(
892
+ prompt_text
893
+ )
894
+
895
+ # -----------------------------------------
896
+ # TOOL CALL STREAM
897
+ # -----------------------------------------
898
+
899
+ if tool_data:
900
+
901
+ pre_text = response_text[
902
+ :tool_data["start"]
903
+ ].strip()
904
+
905
+ tool_call_id = (
906
+ f"call_{uuid.uuid4().hex[:24]}"
907
+ )
908
+
909
+ if pre_text:
910
+
911
+ first_chunk = {
912
+ "id": completion_id,
913
+ "object": (
914
+ "chat.completion.chunk"
915
+ ),
916
+ "created": created,
917
+ "model": model,
918
+ "choices": [
919
+ {
920
+ "index": 0,
921
+ "delta": {
922
+ "role": "assistant",
923
+ "content": "",
924
+ },
925
+ "finish_reason": None,
926
+ }
927
+ ],
928
+ }
929
+
930
+ yield (
931
+ "data: "
932
+ + json.dumps(
933
+ first_chunk,
934
+ ensure_ascii=False,
935
+ )
936
+ + "\n\n"
937
+ )
938
+
939
+ chunk_size = 30
940
+
941
+ for i in range(
942
+ 0,
943
+ len(pre_text),
944
+ chunk_size,
945
+ ):
946
+ piece = pre_text[
947
+ i:i + chunk_size
948
+ ]
949
+
950
+ chunk = {
951
+ "id": completion_id,
952
+ "object": (
953
+ "chat.completion.chunk"
954
+ ),
955
+ "created": created,
956
+ "model": model,
957
+ "choices": [
958
+ {
959
+ "index": 0,
960
+ "delta": {
961
+ "content": piece
962
+ },
963
+ "finish_reason": None,
964
+ }
965
+ ],
966
+ }
967
+
968
+ yield (
969
+ "data: "
970
+ + json.dumps(
971
+ chunk,
972
+ ensure_ascii=False,
973
+ )
974
+ + "\n\n"
975
+ )
976
+
977
+ arguments_json = json.dumps(
978
+ tool_data["arguments"],
979
+ ensure_ascii=False,
980
+ separators=(",", ":"),
981
+ )
982
+
983
+ tool_chunk = {
984
+ "id": completion_id,
985
+ "object": (
986
+ "chat.completion.chunk"
987
+ ),
988
+ "created": created,
989
+ "model": model,
990
+ "choices": [
991
+ {
992
+ "index": 0,
993
+ "delta": {
994
+ "role": "assistant",
995
+ "tool_calls": [
996
+ {
997
+ "index": 0,
998
+ "id": tool_call_id,
999
+ "type": "function",
1000
+ "function": {
1001
+ "name": tool_data["name"],
1002
+ "arguments": arguments_json,
1003
+ },
1004
+ }
1005
+ ],
1006
+ },
1007
+ "finish_reason": None,
1008
+ }
1009
+ ],
1010
+ }
1011
+
1012
+ yield (
1013
+ "data: "
1014
+ + json.dumps(
1015
+ tool_chunk,
1016
+ ensure_ascii=False,
1017
+ )
1018
+ + "\n\n"
1019
+ )
1020
+
1021
+ final_chunk = {
1022
+ "id": completion_id,
1023
+ "object": (
1024
+ "chat.completion.chunk"
1025
+ ),
1026
+ "created": created,
1027
+ "model": model,
1028
+ "choices": [
1029
+ {
1030
+ "index": 0,
1031
+ "delta": {},
1032
+ "finish_reason": (
1033
+ "tool_calls"
1034
+ ),
1035
+ }
1036
+ ],
1037
+ }
1038
+
1039
+ yield (
1040
+ "data: "
1041
+ + json.dumps(
1042
+ final_chunk,
1043
+ ensure_ascii=False,
1044
+ )
1045
+ + "\n\n"
1046
+ )
1047
+
1048
+ if stream_options.get(
1049
+ "include_usage"
1050
+ ):
1051
+
1052
+ completion_tokens = (
1053
+ approximate_tokens(
1054
+ response_text
1055
+ )
1056
+ )
1057
+
1058
+ usage_chunk = {
1059
+ "id": completion_id,
1060
+ "object": (
1061
+ "chat.completion.chunk"
1062
+ ),
1063
+ "created": created,
1064
+ "model": model,
1065
+ "choices": [],
1066
+ "usage": {
1067
+ "prompt_tokens": (
1068
+ prompt_tokens
1069
+ ),
1070
+ "completion_tokens": (
1071
+ completion_tokens
1072
+ ),
1073
+ "total_tokens": (
1074
+ prompt_tokens
1075
+ + completion_tokens
1076
+ ),
1077
+ },
1078
+ }
1079
+
1080
+ yield (
1081
+ "data: "
1082
+ + json.dumps(
1083
+ usage_chunk,
1084
+ ensure_ascii=False,
1085
+ )
1086
+ + "\n\n"
1087
+ )
1088
+
1089
+ yield "data: [DONE]\n\n"
1090
+
1091
+ return
1092
+
1093
+ # -----------------------------------------
1094
+ # NORMAL TEXT STREAM
1095
+ # -----------------------------------------
1096
+
1097
+ initial_chunk = {
1098
+ "id": completion_id,
1099
+ "object": "chat.completion.chunk",
1100
+ "created": created,
1101
+ "model": model,
1102
+ "choices": [
1103
+ {
1104
+ "index": 0,
1105
+ "delta": {
1106
+ "role": "assistant",
1107
+ "content": "",
1108
+ },
1109
+ "finish_reason": None,
1110
+ }
1111
+ ],
1112
+ }
1113
+
1114
+ yield (
1115
+ "data: "
1116
+ + json.dumps(
1117
+ initial_chunk,
1118
+ ensure_ascii=False,
1119
+ )
1120
+ + "\n\n"
1121
+ )
1122
+
1123
+ chunk_size = 30
1124
+
1125
+ for i in range(
1126
+ 0,
1127
+ len(response_text),
1128
+ chunk_size,
1129
+ ):
1130
+ piece = response_text[
1131
+ i:i + chunk_size
1132
+ ]
1133
+
1134
+ chunk = {
1135
+ "id": completion_id,
1136
+ "object": (
1137
+ "chat.completion.chunk"
1138
+ ),
1139
+ "created": created,
1140
+ "model": model,
1141
+ "choices": [
1142
+ {
1143
+ "index": 0,
1144
+ "delta": {
1145
+ "content": piece
1146
+ },
1147
+ "finish_reason": None,
1148
+ }
1149
+ ],
1150
+ }
1151
+
1152
+ yield (
1153
+ "data: "
1154
+ + json.dumps(
1155
+ chunk,
1156
+ ensure_ascii=False,
1157
+ )
1158
+ + "\n\n"
1159
+ )
1160
+
1161
+ await asyncio.sleep(0.005)
1162
+
1163
+ final_chunk = {
1164
+ "id": completion_id,
1165
+ "object": "chat.completion.chunk",
1166
+ "created": created,
1167
+ "model": model,
1168
+ "choices": [
1169
+ {
1170
+ "index": 0,
1171
+ "delta": {},
1172
+ "finish_reason": "stop",
1173
+ }
1174
+ ],
1175
+ }
1176
+
1177
+ yield (
1178
+ "data: "
1179
+ + json.dumps(
1180
+ final_chunk,
1181
+ ensure_ascii=False,
1182
+ )
1183
+ + "\n\n"
1184
+ )
1185
+
1186
+ if stream_options.get(
1187
+ "include_usage"
1188
+ ):
1189
+
1190
+ completion_tokens = (
1191
+ approximate_tokens(
1192
+ response_text
1193
+ )
1194
+ )
1195
+
1196
+ usage_chunk = {
1197
+ "id": completion_id,
1198
+ "object": (
1199
+ "chat.completion.chunk"
1200
+ ),
1201
+ "created": created,
1202
+ "model": model,
1203
+ "choices": [],
1204
+ "usage": {
1205
+ "prompt_tokens": (
1206
+ prompt_tokens
1207
+ ),
1208
+ "completion_tokens": (
1209
+ completion_tokens
1210
+ ),
1211
+ "total_tokens": (
1212
+ prompt_tokens
1213
+ + completion_tokens
1214
+ ),
1215
+ },
1216
+ }
1217
+
1218
+ yield (
1219
+ "data: "
1220
+ + json.dumps(
1221
+ usage_chunk,
1222
+ ensure_ascii=False,
1223
+ )
1224
+ + "\n\n"
1225
+ )
1226
+
1227
+ yield "data: [DONE]\n\n"
1228
+
1229
+ return StreamingResponse(
1230
+ stream_gen(),
1231
+ media_type="text/event-stream",
1232
+ headers={
1233
+ "Cache-Control": "no-cache",
1234
+ "Connection": "keep-alive",
1235
+ "X-Accel-Buffering": "no",
1236
+ },
1237
+ )
1238
+
1239
+ # =================================================
1240
+ # OPENAI NON STREAM
1241
+ # =================================================
1242
+
1243
+ try:
1244
+ response_text = await ask_llm_async(
1245
+ clean_messages
1246
+ )
1247
+
1248
+ except Exception as e:
1249
+ logger.exception(
1250
+ "OpenAI upstream error"
1251
+ )
1252
+
1253
+ return JSONResponse(
1254
+ status_code=502,
1255
+ content=openai_error(
1256
+ str(e),
1257
+ "upstream_error",
1258
+ ),
1259
+ )
1260
+
1261
+ tool_data = extract_tool_call(
1262
+ response_text
1263
+ )
1264
+
1265
+ prompt_tokens = approximate_tokens(
1266
+ json.dumps(
1267
+ clean_messages,
1268
+ ensure_ascii=False,
1269
+ )
1270
+ )
1271
+
1272
+ completion_tokens = approximate_tokens(
1273
+ response_text
1274
+ )
1275
+
1276
+ message_data = {
1277
+ "role": "assistant",
1278
+ "content": response_text,
1279
+ }
1280
+
1281
+ finish_reason = "stop"
1282
+
1283
+ if tool_data:
1284
+
1285
+ pre_text = response_text[
1286
+ :tool_data["start"]
1287
+ ].strip()
1288
+
1289
+ tool_call_id = (
1290
+ f"call_{uuid.uuid4().hex[:24]}"
1291
+ )
1292
+
1293
+ message_data = {
1294
+ "role": "assistant",
1295
+ "content": pre_text or None,
1296
+ "tool_calls": [
1297
+ {
1298
+ "id": tool_call_id,
1299
+ "type": "function",
1300
+ "function": {
1301
+ "name": tool_data["name"],
1302
+ "arguments": json.dumps(
1303
+ tool_data["arguments"],
1304
+ ensure_ascii=False,
1305
+ separators=(",", ":"),
1306
+ ),
1307
+ },
1308
+ }
1309
+ ],
1310
+ }
1311
+
1312
+ finish_reason = "tool_calls"
1313
+
1314
+ return JSONResponse({
1315
+ "id": completion_id,
1316
+ "object": "chat.completion",
1317
+ "created": created,
1318
+ "model": model,
1319
+ "choices": [
1320
+ {
1321
+ "index": 0,
1322
+ "message": message_data,
1323
+ "logprobs": None,
1324
+ "finish_reason": finish_reason,
1325
+ }
1326
+ ],
1327
+ "usage": {
1328
+ "prompt_tokens": prompt_tokens,
1329
+ "completion_tokens": (
1330
+ completion_tokens
1331
+ ),
1332
+ "total_tokens": (
1333
+ prompt_tokens
1334
+ + completion_tokens
1335
+ ),
1336
+ },
1337
+ })
1338
+
1339
+
1340
+ # =====================================================
1341
+ # ANTHROPIC MESSAGES
1342
+ # =====================================================
1343
+
1344
+ @app.api_route(
1345
+ "/v1/messages",
1346
+ methods=["POST"],
1347
+ )
1348
  async def messages_endpoint(request: Request):
1349
  verify(request)
1350
 
1351
  body = await request.json()
1352
+
1353
+ model = body.get(
1354
+ "model",
1355
+ "claude-sonnet-4-6",
1356
+ )
1357
+
1358
+ raw_messages = body.get(
1359
+ "messages",
1360
+ [],
1361
+ )
1362
+
1363
+ do_stream = body.get(
1364
+ "stream",
1365
+ False,
1366
+ )
1367
+
1368
+ client_tools = body.get(
1369
+ "tools",
1370
+ [],
1371
+ )
1372
+
1373
+ system_req = body.get(
1374
+ "system",
1375
+ "",
1376
+ )
1377
 
1378
  if not raw_messages:
1379
+ raise HTTPException(
1380
+ status_code=400,
1381
+ detail="messages required",
1382
+ )
1383
+
1384
+ system_prompt = (
1385
+ system_req
1386
+ if isinstance(system_req, str)
1387
+ else str(system_req)
1388
+ )
1389
 
 
 
1390
  if client_tools:
1391
+
1392
+ tools_str = json.dumps(
1393
+ client_tools,
1394
+ ensure_ascii=False,
1395
+ )
1396
+
1397
+ system_prompt += (
1398
+ "\n\nYou have access to tools. "
1399
+ "To use one, reply ONLY with "
1400
+ "this XML format:\n"
1401
+ '<tool_call>{"name": "tool_name", '
1402
+ '"input": {"param": "value"}}'
1403
+ "</tool_call>\n"
1404
+ f"Available tools: {tools_str}"
1405
+ )
1406
 
1407
  clean_msgs = []
1408
+
1409
  if system_prompt.strip():
1410
+ clean_msgs.append({
1411
+ "role": "system",
1412
+ "content": system_prompt,
1413
+ })
1414
+
1415
+ for message in raw_messages:
1416
+
1417
+ role = message.get(
1418
+ "role",
1419
+ "user",
1420
+ )
1421
+
1422
+ content = message.get(
1423
+ "content",
1424
+ "",
1425
+ )
1426
 
 
 
 
1427
  if isinstance(content, list):
1428
+
1429
+ text = " ".join(
1430
+ block.get("text", "")
1431
+ for block in content
1432
+ if isinstance(block, dict)
1433
+ and block.get("type") == "text"
1434
+ )
1435
+
1436
+ for block in content:
1437
+
1438
+ if (
1439
+ isinstance(block, dict)
1440
+ and block.get("type")
1441
+ == "tool_result"
1442
+ ):
1443
+
1444
+ text += (
1445
+ "\nTool Result "
1446
+ f"[{block.get('tool_use_id')}]: "
1447
+ f"{block.get('content')}"
1448
+ )
1449
+
1450
  else:
1451
  text = str(content)
1452
+
1453
+ clean_msgs.append({
1454
+ "role": role,
1455
+ "content": text,
1456
+ })
1457
+
1458
+ # =================================================
1459
+ # ANTHROPIC STREAM
1460
+ # =================================================
1461
 
1462
  if do_stream:
1463
+
1464
  async def stream_gen():
1465
+
1466
+ msg_id = (
1467
+ f"msg_{uuid.uuid4().hex}"
1468
+ )
1469
+
1470
+ yield (
1471
+ "data: "
1472
+ + json.dumps({
1473
+ "type": "message_start",
1474
+ "message": {
1475
+ "id": msg_id,
1476
+ "type": "message",
1477
+ "role": "assistant",
1478
+ "model": model,
1479
+ "content": [],
1480
+ "stop_reason": None,
1481
+ "usage": {
1482
+ "input_tokens": 0,
1483
+ "output_tokens": 0,
1484
+ },
1485
+ },
1486
+ }, ensure_ascii=False)
1487
+ + "\n\n"
1488
+ )
1489
+
1490
  try:
1491
+ response_text = (
1492
+ await ask_llm_async(
1493
+ clean_msgs
1494
+ )
1495
+ )
1496
+
1497
  except Exception as e:
1498
  response_text = f"Error: {e}"
1499
 
1500
+ tool_data = extract_tool_call(
1501
+ response_text
1502
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1503
 
1504
+ if tool_data:
1505
+
1506
+ pre_text = response_text[
1507
+ :tool_data["start"]
1508
+ ].strip()
1509
+
1510
+ tool_id = (
1511
+ f"toolu_"
1512
+ f"{uuid.uuid4().hex[:24]}"
1513
+ )
1514
+
1515
+ index = 0
1516
+
1517
+ if pre_text:
1518
+
1519
+ yield (
1520
+ "data: "
1521
+ + json.dumps({
1522
+ "type": (
1523
+ "content_block_start"
1524
+ ),
1525
+ "index": index,
1526
+ "content_block": {
1527
+ "type": "text",
1528
+ "text": "",
1529
+ },
1530
+ }, ensure_ascii=False)
1531
+ + "\n\n"
1532
+ )
1533
+
1534
+ chunk_size = 30
1535
+
1536
+ for i in range(
1537
+ 0,
1538
+ len(pre_text),
1539
+ chunk_size,
1540
+ ):
1541
+
1542
+ piece = pre_text[
1543
+ i:i + chunk_size
1544
+ ]
1545
+
1546
+ yield (
1547
+ "data: "
1548
+ + json.dumps({
1549
+ "type": (
1550
+ "content_block_delta"
1551
+ ),
1552
+ "index": index,
1553
+ "delta": {
1554
+ "type": "text_delta",
1555
+ "text": piece,
1556
+ },
1557
+ }, ensure_ascii=False)
1558
+ + "\n\n"
1559
+ )
1560
+
1561
+ yield (
1562
+ "data: "
1563
+ + json.dumps({
1564
+ "type": (
1565
+ "content_block_stop"
1566
+ ),
1567
+ "index": index,
1568
+ })
1569
+ + "\n\n"
1570
+ )
1571
+
1572
+ index += 1
1573
+
1574
+ yield (
1575
+ "data: "
1576
+ + json.dumps({
1577
+ "type": (
1578
+ "content_block_start"
1579
+ ),
1580
+ "index": index,
1581
+ "content_block": {
1582
+ "type": "tool_use",
1583
+ "id": tool_id,
1584
+ "name": tool_data["name"],
1585
+ "input": {},
1586
+ },
1587
+ }, ensure_ascii=False)
1588
+ + "\n\n"
1589
+ )
1590
+
1591
+ yield (
1592
+ "data: "
1593
+ + json.dumps({
1594
+ "type": (
1595
+ "content_block_delta"
1596
+ ),
1597
+ "index": index,
1598
+ "delta": {
1599
+ "type": (
1600
+ "input_json_delta"
1601
+ ),
1602
+ "partial_json": (
1603
+ json.dumps(
1604
+ tool_data[
1605
+ "arguments"
1606
+ ],
1607
+ ensure_ascii=False,
1608
+ )
1609
+ ),
1610
+ },
1611
+ }, ensure_ascii=False)
1612
+ + "\n\n"
1613
+ )
1614
+
1615
+ yield (
1616
+ "data: "
1617
+ + json.dumps({
1618
+ "type": (
1619
+ "content_block_stop"
1620
+ ),
1621
+ "index": index,
1622
+ })
1623
+ + "\n\n"
1624
+ )
1625
+
1626
+ yield (
1627
+ "data: "
1628
+ + json.dumps({
1629
+ "type": "message_delta",
1630
+ "delta": {
1631
+ "stop_reason": "tool_use",
1632
+ "stop_sequence": None,
1633
+ },
1634
+ "usage": {
1635
+ "output_tokens": (
1636
+ approximate_tokens(
1637
+ response_text
1638
+ )
1639
+ ),
1640
+ },
1641
+ }, ensure_ascii=False)
1642
+ + "\n\n"
1643
+ )
1644
+
1645
+ yield (
1646
+ "data: "
1647
+ + json.dumps({
1648
+ "type": "message_stop"
1649
+ })
1650
+ + "\n\n"
1651
+ )
1652
+
1653
+ return
1654
+
1655
+ yield (
1656
+ "data: "
1657
+ + json.dumps({
1658
+ "type": "content_block_start",
1659
+ "index": 0,
1660
+ "content_block": {
1661
+ "type": "text",
1662
+ "text": "",
1663
+ },
1664
+ }, ensure_ascii=False)
1665
+ + "\n\n"
1666
+ )
1667
 
 
 
1668
  chunk_size = 30
1669
+
1670
+ for i in range(
1671
+ 0,
1672
+ len(response_text),
1673
+ chunk_size,
1674
+ ):
1675
+
1676
+ piece = response_text[
1677
+ i:i + chunk_size
1678
+ ]
1679
+
1680
+ yield (
1681
+ "data: "
1682
+ + json.dumps({
1683
+ "type": "content_block_delta",
1684
+ "index": 0,
1685
+ "delta": {
1686
+ "type": "text_delta",
1687
+ "text": piece,
1688
+ },
1689
+ }, ensure_ascii=False)
1690
+ + "\n\n"
1691
+ )
1692
+
1693
  await asyncio.sleep(0.005)
1694
 
1695
+ yield (
1696
+ "data: "
1697
+ + json.dumps({
1698
+ "type": "content_block_stop",
1699
+ "index": 0,
1700
+ })
1701
+ + "\n\n"
1702
+ )
1703
+
1704
+ yield (
1705
+ "data: "
1706
+ + json.dumps({
1707
+ "type": "message_delta",
1708
+ "delta": {
1709
+ "stop_reason": "end_turn",
1710
+ "stop_sequence": None,
1711
+ },
1712
+ "usage": {
1713
+ "output_tokens": (
1714
+ approximate_tokens(
1715
+ response_text
1716
+ )
1717
+ ),
1718
+ },
1719
+ }, ensure_ascii=False)
1720
+ + "\n\n"
1721
+ )
1722
+
1723
+ yield (
1724
+ "data: "
1725
+ + json.dumps({
1726
+ "type": "message_stop"
1727
+ })
1728
+ + "\n\n"
1729
+ )
1730
 
1731
  return StreamingResponse(
1732
  stream_gen(),
1733
  media_type="text/event-stream",
1734
+ headers={
1735
+ "Cache-Control": "no-cache",
1736
+ "Connection": "keep-alive",
1737
+ "X-Accel-Buffering": "no",
1738
+ },
1739
  )
1740
+
1741
+ # =================================================
1742
+ # ANTHROPIC NON STREAM
1743
+ # =================================================
1744
+
1745
+ response_text = await ask_llm_async(
1746
+ clean_msgs
1747
+ )
1748
+
1749
+ tool_data = extract_tool_call(
1750
+ response_text
1751
+ )
1752
+
1753
+ content_blocks = []
1754
+
1755
+ stop_reason = "end_turn"
1756
+
1757
+ if tool_data:
1758
+
1759
+ pre_text = response_text[
1760
+ :tool_data["start"]
1761
+ ].strip()
1762
+
1763
+ if pre_text:
1764
+ content_blocks.append({
1765
+ "type": "text",
1766
+ "text": pre_text,
1767
+ })
1768
+
1769
+ content_blocks.append({
1770
+ "type": "tool_use",
1771
+ "id": (
1772
+ f"toolu_{uuid.uuid4().hex[:24]}"
1773
+ ),
1774
+ "name": tool_data["name"],
1775
+ "input": tool_data["arguments"],
1776
+ })
1777
+
1778
+ stop_reason = "tool_use"
1779
+
1780
  else:
1781
+ content_blocks.append({
1782
+ "type": "text",
1783
+ "text": response_text,
 
 
 
 
 
 
 
1784
  })
1785
 
1786
+ return JSONResponse({
1787
+ "id": f"msg_{uuid.uuid4().hex}",
1788
+ "type": "message",
1789
+ "role": "assistant",
1790
+ "model": model,
1791
+ "content": content_blocks,
1792
+ "stop_reason": stop_reason,
1793
+ "stop_sequence": None,
1794
+ "usage": {
1795
+ "input_tokens": approximate_tokens(
1796
+ json.dumps(
1797
+ clean_msgs,
1798
+ ensure_ascii=False,
1799
+ )
1800
+ ),
1801
+ "output_tokens": approximate_tokens(
1802
+ response_text
1803
+ ),
1804
+ },
1805
+ })
1806
+
1807
+
1808
+ # =====================================================
1809
+ # ENTRY POINT
1810
+ # =====================================================
1811
+
1812
  if __name__ == "__main__":
1813
  import uvicorn
1814
+
1815
+ port = int(
1816
+ os.getenv(
1817
+ "PORT",
1818
+ "7860",
1819
+ )
1820
+ )
1821
+
1822
+ uvicorn.run(
1823
+ app,
1824
+ host="0.0.0.0",
1825
+ port=port,
1826
+ )