web2app / app.py
bahi-bh's picture
Update app.py
2d2c4be verified
Raw History Blame Contribute Delete
60.1 kB
import os
import base64
import ctypes
import json
import logging
import queue
import random
import re
import struct
import threading
import time
import sys
import urllib.request
from curl_cffi import requests
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from wasmtime import Linker, Module, Store
# ============================================================
# Configuration
# ============================================================
HOST = "0.0.0.0"
PORT = int(os.getenv("PORT", "7860"))
DEEPSEEK_EMAIL = os.getenv("DEEPSEEK_EMAIL", "").strip()
DEEPSEEK_PASSWORD = os.getenv("DEEPSEEK_PASSWORD", "").strip()
DEEPSEEK_EMAIL_2 = os.getenv("DEEPSEEK_EMAIL_2", "").strip()
DEEPSEEK_PASSWORD_2 = os.getenv("DEEPSEEK_PASSWORD_2", "").strip()
ACCOUNTS = []
if DEEPSEEK_EMAIL and DEEPSEEK_PASSWORD:
ACCOUNTS.append({
"email": DEEPSEEK_EMAIL,
"password": DEEPSEEK_PASSWORD,
"token": ""
})
if DEEPSEEK_EMAIL_2 and DEEPSEEK_PASSWORD_2:
ACCOUNTS.append({
"email": DEEPSEEK_EMAIL_2,
"password": DEEPSEEK_PASSWORD_2,
"token": ""
})
WASM_PATH = os.getenv("WASM_PATH", "./sha3_wasm_bg.7b9ca65ddd.wasm")
WASM_URL = "https://raw.githubusercontent.com/iidamie/deepseek2api/main/sha3_wasm_bg.7b9ca65ddd.wasm"
KEEP_ALIVE_TIMEOUT = 5
CLAUDE_DEFAULT_MODEL = "claude-sonnet-4-20250514"
# ============================================================
# Logging
# ============================================================
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
logger = logging.getLogger("deepseek-api")
# ============================================================
# FastAPI App
# ============================================================
app = FastAPI(
title="DeepSeek to API",
version="1.0.0"
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["GET", "POST", "OPTIONS", "PUT", "DELETE"],
allow_headers=["*"],
)
templates = Jinja2Templates(directory="templates")
# ============================================================
# WASM download helper
# ============================================================
def download_wasm_file():
try:
logger.info(f"محاولة تحميل WASM من: {WASM_URL}")
urllib.request.urlretrieve(WASM_URL, WASM_PATH)
if os.path.isfile(WASM_PATH):
logger.info(f"تم تحميل WASM بنجاح")
return True
return False
except Exception as e:
logger.error(f"فشل تحميل WASM: {e}")
return False
# ============================================================
# DeepSeek endpoints
# ============================================================
DEEPSEEK_HOST = "chat.deepseek.com"
DEEPSEEK_LOGIN_URL = f"https://{DEEPSEEK_HOST}/api/v0/users/login"
DEEPSEEK_CREATE_SESSION_URL = f"https://{DEEPSEEK_HOST}/api/v0/chat_session/create"
DEEPSEEK_CREATE_POW_URL = f"https://{DEEPSEEK_HOST}/api/v0/chat/create_pow_challenge"
DEEPSEEK_COMPLETION_URL = f"https://{DEEPSEEK_HOST}/api/v0/chat/completion"
DEEPSEEK_STOP_STREAM_URL = f"https://{DEEPSEEK_HOST}/api/v0/chat/stop_stream"
DEEPSEEK_DELETE_SESSION_URL = f"https://{DEEPSEEK_HOST}/api/v0/chat_session/delete"
BASE_HEADERS = {
"Host": DEEPSEEK_HOST,
"User-Agent": "DeepSeek/2.0.0 Android/35",
"Accept": "application/json",
"Accept-Encoding": "gzip",
"Content-Type": "application/json",
"x-client-platform": "android",
"x-client-version": "2.0.0",
"x-client-locale": "zh_CN",
"accept-charset": "UTF-8",
}
# ============================================================
# Startup event
# ============================================================
@app.on_event("startup")
async def startup_event():
logger.info("=" * 60)
logger.info("DeepSeek API starting...")
logger.info("Port: %s", PORT)
logger.info("WASM path: %s", os.path.abspath(WASM_PATH))
logger.info("Number of accounts configured: %d", len(ACCOUNTS))
if ACCOUNTS:
for acc in ACCOUNTS:
logger.info(" - %s", acc.get("email"))
else:
logger.warning("No accounts configured.")
if not os.path.isfile(WASM_PATH):
logger.warning("WASM file missing, attempting download...")
if download_wasm_file():
logger.info("WASM downloaded successfully")
else:
logger.critical("Failed to download WASM file")
logger.info("=" * 60)
# ============================================================
# Authentication
# ============================================================
account_queue = ACCOUNTS.copy()
random.shuffle(account_queue)
token_cache = {}
token_refresh_time = {}
def login_deepseek_via_account(account):
email = account.get("email", "").strip()
password = account.get("password", "").strip()
if not email or not password:
raise HTTPException(status_code=400, detail="الحساب يجب أن يحتوي على email و password")
payload = {
"email": email,
"password": password,
"device_id": "deepseek_to_api",
"os": "android",
}
logger.info(f"تسجيل الدخول للحساب: {email}")
try:
resp = requests.post(
DEEPSEEK_LOGIN_URL,
headers=BASE_HEADERS,
json=payload,
impersonate="safari15_3",
timeout=30
)
try:
data = resp.json()
except Exception as e:
logger.error(f"فشل تحليل الاستجابة: {e}")
raise HTTPException(status_code=500, detail="استجابة غير صالحة")
logger.info(f"استجابة تسجيل الدخول: code={data.get('code')}, msg={data.get('msg', 'N/A')}")
if resp.status_code != 200 or data.get("code") != 0:
error_msg = data.get("msg", "خطأ غير معروف")
logger.error(f"فشل تسجيل الدخول: {error_msg}")
raise HTTPException(status_code=401, detail=f"فشل تسجيل الدخول: {error_msg}")
user_data = data.get("data", {}).get("biz_data", {}).get("user")
if not user_data:
logger.error(f"استجابة غير مكتملة: {data}")
raise HTTPException(status_code=500, detail="استجابة غير مكتملة")
new_token = user_data.get("token")
if not new_token:
raise HTTPException(status_code=500, detail="لم يتم الحصول على token")
account["token"] = new_token
email = account.get("email")
token_cache[email] = new_token
token_refresh_time[email] = time.time()
logger.info(f"تم تسجيل الدخول بنجاح للحساب {email}")
return new_token
except HTTPException:
raise
except Exception as e:
logger.error(f"خطأ في تسجيل الدخول: {e}")
raise HTTPException(status_code=500, detail=str(e))
def get_valid_token(force_refresh=False):
if not account_queue:
raise HTTPException(status_code=500, detail="لا توجد حسابات متاحة")
account = account_queue.pop(0)
email = account.get("email", "unknown")
if not force_refresh and email in token_cache and token_cache[email]:
last_refresh = token_refresh_time.get(email, 0)
if time.time() - last_refresh < 1800:
logger.info(f"استخدام token مخزن للحساب: {email}")
token = token_cache[email]
account["token"] = token
account_queue.append(account)
return token, account
else:
logger.info(f"انتهت صلاحية التوكن للحساب {email}، تحديث...")
try:
token = login_deepseek_via_account(account)
account_queue.append(account)
return token, account
except Exception as e:
logger.error(f"فشل تسجيل الدخول للحساب {email}: {e}")
if account_queue:
return get_valid_token(force_refresh=True)
raise
def get_deepseek_token(force_refresh=False):
try:
return get_valid_token(force_refresh)
except HTTPException:
raise
except Exception as e:
logger.error(f"خطأ في get_deepseek_token: {e}")
raise HTTPException(status_code=500, detail="خطأ في المصادقة")
def determine_mode_and_token(request: Request, force_refresh=False):
try:
token, account = get_deepseek_token(force_refresh)
request.state.deepseek_token = token
request.state.account = account
logger.info(f"تم تعيين التوكن للطلب")
except Exception as e:
logger.error(f"فشل تعيين التوكن: {e}")
raise
def determine_claude_mode_and_token(request: Request):
determine_mode_and_token(request)
def get_auth_headers(request: Request):
token = getattr(request.state, "deepseek_token", None)
if not token:
token, _ = get_deepseek_token()
request.state.deepseek_token = token
return {
**BASE_HEADERS,
"authorization": f"Bearer {token}"
}
def force_refresh_token(request: Request):
try:
token, account = get_deepseek_token(force_refresh=True)
request.state.deepseek_token = token
request.state.account = account
logger.info(f"تم تحديث التوكن بنجاح")
return token
except Exception as e:
logger.error(f"فشل تحديث التوكن: {e}")
return None
# ============================================================
# Utility functions - IMPROVED
# ============================================================
def safe_json_response_error(status_code: int, message: str):
return JSONResponse(
status_code=status_code,
content={"error": {"message": message, "type": "api_error"}}
)
def estimate_tokens(value):
if value is None:
return 0
if isinstance(value, str):
return max(1, len(value) // 4)
if isinstance(value, list):
return sum(estimate_tokens(item) for item in value)
if isinstance(value, dict):
return estimate_tokens(json.dumps(value, ensure_ascii=False))
return max(1, len(str(value)) // 4)
def normalize_message_content(content):
if isinstance(content, str):
return content
if isinstance(content, list):
parts = []
for item in content:
if not isinstance(item, dict):
continue
item_type = item.get("type")
if item_type == "text":
parts.append(str(item.get("text", "")))
elif item_type == "tool_result":
result_content = item.get("content", "")
if isinstance(result_content, list):
for sub in result_content:
if isinstance(sub, dict) and sub.get("type") == "text":
parts.append(str(sub.get("text", "")))
else:
parts.append(str(result_content))
return "\n".join(parts)
return str(content)
def messages_prepare(messages: list) -> str:
"""
تحضير الرسائل لتكون متوافقة مع تنسيق DeepSeek
"""
if not messages:
return ""
processed = []
for message in messages:
if not isinstance(message, dict):
continue
role = message.get("role", "user")
content = normalize_message_content(message.get("content", ""))
if content: # نتجاهل الرسائل الفارغة
processed.append({"role": role, "text": content})
if not processed:
return "مرحباً، كيف يمكنني مساعدتك؟"
# دمج الرسائل المتتالية من نفس الدور
merged = [processed[0]]
for message in processed[1:]:
if message["role"] == merged[-1]["role"]:
merged[-1]["text"] += "\n\n" + message["text"]
else:
merged.append(message)
parts = []
for index, block in enumerate(merged):
role = block["role"]
text = block["text"]
if role == "assistant":
parts.append(f"<|Assistant|>{text}<|end▁of▁sentence|>")
elif role in ("user", "system"):
if index > 0:
parts.append(f"<|User|>{text}")
else:
parts.append(text)
else:
parts.append(text)
final_prompt = "".join(parts)
# التأكد من وجود محتوى
if not final_prompt.strip():
return "مرحباً، كيف يمكنني مساعدتك؟"
return final_prompt
def get_model_features(model: str):
model_lower = model.lower()
if model_lower in ("deepseek-v3", "deepseek-chat"):
return False, False
if model_lower in ("deepseek-r1", "deepseek-reasoner"):
return True, False
if model_lower in ("deepseek-v3-search", "deepseek-chat-search"):
return False, True
if model_lower in ("deepseek-r1-search", "deepseek-reasoner-search"):
return True, True
raise HTTPException(status_code=503, detail=f"Model '{model}' is not available.")
def detect_and_parse_tool_calls(content: str):
if not content:
return None, content
cleaned = content.strip()
try:
parsed = json.loads(cleaned)
if isinstance(parsed, dict) and isinstance(parsed.get("tool_calls"), list):
calls = []
for index, call in enumerate(parsed["tool_calls"]):
if not isinstance(call, dict):
continue
function = call.get("function")
if function:
name = function.get("name")
if not name:
continue
arguments = function.get("arguments", "{}")
if isinstance(arguments, dict):
arguments = json.dumps(arguments, ensure_ascii=False)
calls.append({
"id": call.get("id", f"call_{index+1:03d}"),
"type": call.get("type", "function"),
"function": {"name": name, "arguments": arguments}
})
else:
name = call.get("name")
if not name:
continue
arguments = call.get("input", {})
calls.append({
"id": call.get("id", f"call_{index+1:03d}"),
"type": "function",
"function": {
"name": name,
"arguments": json.dumps(arguments, ensure_ascii=False)
}
})
if calls:
return calls, ""
except Exception:
pass
pattern = r'\{\s*"tool_calls"\s*:\s*\[(.*?)\]\s*\}'
match = re.search(pattern, cleaned, re.DOTALL)
if match:
try:
obj = json.loads(match.group(0))
calls = obj.get("tool_calls", [])
if calls:
normalized = []
for index, call in enumerate(calls):
if not isinstance(call, dict):
continue
name = call.get("name")
if name:
normalized.append({
"id": call.get("id", f"call_{index+1:03d}"),
"type": "function",
"function": {
"name": name,
"arguments": json.dumps(call.get("input", {}), ensure_ascii=False)
}
})
elif isinstance(call.get("function"), dict):
function = call["function"]
name = function.get("name")
if not name:
continue
args = function.get("arguments", "{}")
if isinstance(args, dict):
args = json.dumps(args, ensure_ascii=False)
normalized.append({
"id": call.get("id", f"call_{index+1:03d}"),
"type": "function",
"function": {"name": name, "arguments": args}
})
if normalized:
remaining = (cleaned[:match.start()] + cleaned[match.end():]).strip()
return normalized, remaining
except Exception:
pass
return None, content
def build_openai_tool_prompt(tools):
schemas = []
for tool in tools:
function = tool.get("function", {})
name = function.get("name", "unknown")
description = function.get("description", "")
parameters = function.get("parameters", {})
info = f"Tool: {name}\nDescription: {description}"
properties = parameters.get("properties", {})
required = parameters.get("required", [])
if properties:
lines = []
for prop_name, prop in properties.items():
prop_type = prop.get("type", "string")
prop_desc = prop.get("description", "")
required_text = " (required)" if prop_name in required else ""
lines.append(f" - {prop_name}: {prop_type}{required_text} - {prop_desc}")
info += "\nParameters:\n" + "\n".join(lines)
schemas.append(info)
return f"""
You have access to the following tools:
{chr(10).join(schemas)}
When you need to call a tool, output ONLY valid JSON:
{{"tool_calls":[{{"id":"call_001","type":"function","function":{{"name":"tool_name","arguments":"{{\\"param\\":\\"value\\"}}"}}}}]}}
The arguments field MUST be a JSON string.
You may call multiple tools in one response.
Do not write explanations before or after the JSON.
"""
# ============================================================
# DeepSeek completion helpers
# ============================================================
def call_completion_endpoint(payload, headers, max_attempts=3):
for attempt in range(max_attempts):
try:
response = requests.post(
DEEPSEEK_COMPLETION_URL,
headers=headers,
json=payload,
stream=True,
impersonate="safari15_3",
timeout=120
)
if response.status_code == 200:
return response
logger.warning("Completion failed: HTTP %s", response.status_code)
response.close()
except Exception as exc:
logger.warning("Completion request error: %s", exc)
if attempt < max_attempts - 1:
time.sleep(1)
return None
def create_session(request: Request, max_attempts=2):
for attempt in range(max_attempts):
headers = get_auth_headers(request)
try:
response = requests.post(
DEEPSEEK_CREATE_SESSION_URL,
headers=headers,
json={"agent": "chat"},
impersonate="safari15_3",
timeout=30
)
try:
data = response.json()
except Exception as e:
logger.error(f"فشل تحليل الاستجابة: {e}")
data = {}
if response.status_code == 200 and data.get("code") == 0:
biz_data = data.get("data", {}).get("biz_data", {})
chat_session = biz_data.get("chat_session", {})
session_id = chat_session.get("id")
if session_id:
response.close()
logger.info(f"تم إنشاء الجلسة: {session_id}")
return session_id
else:
logger.warning(f"session_id غير موجود في الاستجابة")
if attempt == 0:
logger.info("محاولة تحديث التوكن...")
if force_refresh_token(request):
continue
else:
error_msg = data.get("msg", f"HTTP {response.status_code}")
logger.warning(f"Create session failed: {error_msg}")
response.close()
if response.status_code == 401 and attempt == 0:
if force_refresh_token(request):
continue
except Exception as exc:
logger.warning(f"Create session error: {exc}")
if attempt < max_attempts - 1:
time.sleep(1)
return None
def compute_pow_answer(algorithm, challenge_str, salt, difficulty, expire_at, signature, target_path, wasm_path):
if algorithm != "DeepSeekHashV1":
raise ValueError(f"Unsupported PoW algorithm: {algorithm}")
if not os.path.isfile(wasm_path):
if download_wasm_file():
logger.info("WASM downloaded successfully")
else:
raise FileNotFoundError(f"WASM file not found: {wasm_path}")
prefix = f"{salt}_{expire_at}_"
store = Store()
linker = Linker(store.engine)
with open(wasm_path, "rb") as f:
wasm_bytes = f.read()
module = Module(store.engine, wasm_bytes)
instance = linker.instantiate(store, module)
exports = instance.exports(store)
try:
memory = exports["memory"]
add_to_stack = exports["__wbindgen_add_to_stack_pointer"]
alloc = exports["__wbindgen_export_0"]
wasm_solve = exports["wasm_solve"]
except KeyError as exc:
raise RuntimeError(f"Missing WASM export: {exc}")
def memory_address():
return ctypes.cast(memory.data_ptr(store), ctypes.c_void_p).value
def write_memory(offset, data):
ctypes.memmove(memory_address() + offset, data, len(data))
def read_memory(offset, size):
return ctypes.string_at(memory_address() + offset, size)
def encode_string(text):
data = text.encode("utf-8")
result = alloc(store, len(data), 1)
ptr = int(result.value) if hasattr(result, "value") else int(result)
write_memory(ptr, data)
return ptr, len(data)
retptr = add_to_stack(store, -16)
try:
ptr_challenge, len_challenge = encode_string(challenge_str)
ptr_prefix, len_prefix = encode_string(prefix)
wasm_solve(store, retptr, ptr_challenge, len_challenge, ptr_prefix, len_prefix, float(difficulty))
status_bytes = read_memory(retptr, 4)
if len(status_bytes) != 4:
raise RuntimeError("Unable to read PoW status")
status = struct.unpack("<i", status_bytes)[0]
value_bytes = read_memory(retptr + 8, 8)
if len(value_bytes) != 8:
raise RuntimeError("Unable to read PoW result")
value = struct.unpack("<d", value_bytes)[0]
if status == 0:
return None
return int(value)
finally:
add_to_stack(store, 16)
def get_pow_response(request: Request, max_attempts=2):
for attempt in range(max_attempts):
headers = get_auth_headers(request)
try:
response = requests.post(
DEEPSEEK_CREATE_POW_URL,
headers=headers,
json={"target_path": "/api/v0/chat/completion"},
timeout=30,
impersonate="safari15_3"
)
try:
data = response.json()
except Exception:
data = {}
if response.status_code == 200 and data.get("code") == 0:
challenge = data.get("data", {}).get("biz_data", {}).get("challenge", {})
algorithm = challenge.get("algorithm")
challenge_str = challenge.get("challenge")
salt = challenge.get("salt")
difficulty = challenge.get("difficulty", 144000)
expire_at = challenge.get("expire_at", 1680000000)
signature = challenge.get("signature")
target_path = challenge.get("target_path", "/api/v0/chat/completion")
answer = compute_pow_answer(
algorithm, challenge_str, salt, difficulty, expire_at,
signature, target_path, WASM_PATH
)
if answer is None:
response.close()
logger.warning("PoW returned no answer")
continue
pow_payload = {
"algorithm": algorithm,
"challenge": challenge_str,
"salt": salt,
"answer": answer,
"signature": signature,
"target_path": target_path
}
encoded = base64.b64encode(
json.dumps(pow_payload, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
).decode("utf-8")
response.close()
logger.info("تم الحصول على PoW بنجاح")
return encoded
elif response.status_code == 401 and attempt == 0:
logger.warning("PoW: 401 Unauthorized, refreshing token...")
if force_refresh_token(request):
continue
else:
logger.warning("PoW request failed: HTTP=%s code=%s", response.status_code, data.get("code"))
response.close()
except Exception as exc:
logger.error("PoW error: %s", exc)
if attempt < max_attempts - 1:
time.sleep(1)
return None
def delete_session(request: Request, session_id: str):
if not session_id:
return
try:
response = requests.post(
DEEPSEEK_DELETE_SESSION_URL,
headers=get_auth_headers(request),
json={"chat_session_id": session_id},
impersonate="safari15_3",
timeout=5
)
if response.status_code == 200:
logger.info("Deleted session: %s", session_id)
response.close()
except Exception as exc:
logger.warning("Delete session error: %s", exc)
def parse_deepseek_stream(response, on_text=None, on_thinking=None):
current_type = "text"
has_content = False
try:
for raw_line in response.iter_lines():
if not raw_line:
continue
try:
line = raw_line.decode("utf-8")
except Exception:
continue
if not line.startswith("data:"):
continue
data_str = line[5:].strip()
if data_str == "[DONE]":
break
try:
chunk = json.loads(data_str)
except Exception:
continue
if "v" not in chunk:
continue
value = chunk["v"]
path = chunk.get("p", "")
if path == "response/status":
if value == "FINISHED":
break
continue
if path == "response/search_status":
continue
if path == "response/thinking_content":
current_type = "thinking"
has_content = True
elif path == "response/content":
current_type = "text"
has_content = True
if isinstance(value, str):
if current_type == "thinking" and on_thinking:
on_thinking(value)
has_content = True
elif current_type == "text" and on_text:
on_text(value)
has_content = True
elif isinstance(value, list):
finished = False
for item in value:
if isinstance(item, dict) and item.get("p") == "status" and item.get("v") == "FINISHED":
finished = True
if finished:
break
finally:
try:
response.close()
except Exception:
pass
return has_content
# ============================================================
# Endpoints with improved empty response handling
# ============================================================
@app.get("/v1/models")
async def list_models():
models = [
{"id": "deepseek-chat", "object": "model", "created": 1677610602, "owned_by": "deepseek"},
{"id": "deepseek-reasoner", "object": "model", "created": 1677610602, "owned_by": "deepseek"},
{"id": "deepseek-chat-search", "object": "model", "created": 1677610602, "owned_by": "deepseek"},
{"id": "deepseek-reasoner-search", "object": "model", "created": 1677610602, "owned_by": "deepseek"}
]
return {"object": "list", "data": models}
@app.post("/v1/chat/completions")
async def chat_completions(request: Request):
retry_count = 0
max_retries = 3 # زيادة عدد المحاولات
while retry_count <= max_retries:
try:
if retry_count > 0:
logger.info(f"محاولة إعادة الطلب #{retry_count}، تحديث التوكن...")
force_refresh_token(request)
# ننتظر قليلاً بين المحاولات
time.sleep(1)
determine_mode_and_token(request)
req_data = await request.json()
model = req_data.get("model", "deepseek-chat")
messages = req_data.get("messages", [])
if not messages:
# إذا لم تكن هناك رسائل، نضيف رسالة افتراضية
messages = [{"role": "user", "content": "مرحباً"}]
# طباعة الطلب للتصحيح
logger.info(f"الطلب: model={model}, messages={json.dumps(messages, ensure_ascii=False)[:200]}")
thinking_enabled, search_enabled = get_model_features(model)
tools = req_data.get("tools") or []
has_tools = bool(tools)
messages = [dict(msg) for msg in messages]
if has_tools:
tool_prompt = build_openai_tool_prompt(tools)
system_found = False
for msg in messages:
if msg.get("role") == "system":
old_content = normalize_message_content(msg.get("content", ""))
msg["content"] = old_content + "\n\n" + tool_prompt
system_found = True
break
if not system_found:
messages.insert(0, {"role": "system", "content": tool_prompt})
final_prompt = messages_prepare(messages)
logger.info(f"الـ prompt النهائي: {final_prompt[:200]}...")
session_id = create_session(request)
if not session_id:
if retry_count < max_retries:
retry_count += 1
continue
return safe_json_response_error(401, "Unable to create DeepSeek session.")
pow_response = get_pow_response(request)
if not pow_response:
delete_session(request, session_id)
if retry_count < max_retries:
retry_count += 1
continue
return safe_json_response_error(401, "Unable to obtain DeepSeek PoW.")
headers = {
**get_auth_headers(request),
"x-ds-pow-response": pow_response
}
payload = {
"chat_session_id": session_id,
"parent_message_id": None,
"prompt": final_prompt,
"ref_file_ids": [],
"thinking_enabled": thinking_enabled,
"search_enabled": search_enabled
}
deepseek_response = call_completion_endpoint(payload, headers, max_attempts=3)
if not deepseek_response:
delete_session(request, session_id)
if retry_count < max_retries:
retry_count += 1
continue
return safe_json_response_error(500, "Failed to get completion.")
created = int(time.time())
completion_id = f"chatcmpl-{session_id}"
# Streaming
if req_data.get("stream", False):
def stream_generator():
final_text = []
final_thinking = []
first_chunk = True
has_content = False
try:
current_type = "text"
for raw_line in deepseek_response.iter_lines():
if not raw_line:
continue
try:
line = raw_line.decode("utf-8")
except Exception:
continue
if not line.startswith("data:"):
continue
data_str = line[5:].strip()
if data_str == "[DONE]":
break
try:
chunk = json.loads(data_str)
except Exception:
continue
if "v" not in chunk:
continue
value = chunk["v"]
path = chunk.get("p", "")
if path == "response/status":
if value == "FINISHED":
break
continue
if path == "response/search_status":
continue
if path == "response/thinking_content":
current_type = "thinking"
elif path == "response/content":
current_type = "text"
if not isinstance(value, str):
continue
if search_enabled and value.startswith("[citation:"):
continue
delta = {}
if first_chunk:
delta["role"] = "assistant"
first_chunk = False
if current_type == "thinking":
if thinking_enabled:
final_thinking.append(value)
delta["reasoning_content"] = value
has_content = True
else:
final_text.append(value)
delta["content"] = value
has_content = True
if not delta:
continue
output_chunk = {
"id": completion_id,
"object": "chat.completion.chunk",
"created": created,
"model": model,
"choices": [{"index": 0, "delta": delta, "finish_reason": None}]
}
yield f"data: {json.dumps(output_chunk, ensure_ascii=False)}\n\n"
# إذا لم يكن هناك محتوى، نرسل رسالة مفيدة
if not has_content:
logger.warning("استجابة فارغة من DeepSeek، استخدام رد افتراضي")
default_responses = [
"مرحباً! كيف يمكنني مساعدتك اليوم؟",
"أهلاً بك! ما الذي تريد معرفته؟",
"مرحباً! أنا هنا للإجابة على أسئلتك."
]
default_text = random.choice(default_responses)
delta = {"role": "assistant", "content": default_text}
output_chunk = {
"id": completion_id,
"object": "chat.completion.chunk",
"created": created,
"model": model,
"choices": [{"index": 0, "delta": delta, "finish_reason": None}]
}
yield f"data: {json.dumps(output_chunk, ensure_ascii=False)}\n\n"
final_text.append(default_text)
full_text = "".join(final_text)
tool_calls = None
clean_text = full_text
if has_tools:
tool_calls, clean_text = detect_and_parse_tool_calls(full_text)
if tool_calls:
for tool_call in tool_calls:
tool_chunk = {
"id": completion_id,
"object": "chat.completion.chunk",
"created": created,
"model": model,
"choices": [{
"index": 0,
"delta": {"tool_calls": [tool_call]},
"finish_reason": None
}]
}
yield f"data: {json.dumps(tool_chunk, ensure_ascii=False)}\n\n"
prompt_tokens = estimate_tokens(final_prompt)
reasoning_tokens = estimate_tokens("".join(final_thinking))
completion_tokens = estimate_tokens(clean_text)
finish_reason = "tool_calls" if tool_calls else "stop"
finish_chunk = {
"id": completion_id,
"object": "chat.completion.chunk",
"created": created,
"model": model,
"choices": [{"index": 0, "delta": {}, "finish_reason": finish_reason}],
"usage": {
"prompt_tokens": prompt_tokens,
"completion_tokens": reasoning_tokens + completion_tokens,
"total_tokens": prompt_tokens + reasoning_tokens + completion_tokens,
"completion_tokens_details": {"reasoning_tokens": reasoning_tokens}
}
}
yield f"data: {json.dumps(finish_chunk, ensure_ascii=False)}\n\n"
yield "data: [DONE]\n\n"
except Exception as exc:
logger.exception("OpenAI stream error")
error_chunk = {"error": {"message": "Stream processing error", "type": "server_error"}}
yield f"data: {json.dumps(error_chunk)}\n\n"
finally:
try:
deepseek_response.close()
except Exception:
pass
delete_session(request, session_id)
return StreamingResponse(
stream_generator(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"}
)
# Non-streaming
final_text = []
final_thinking = []
has_content = parse_deepseek_stream(
deepseek_response,
on_text=final_text.append,
on_thinking=final_thinking.append
)
text = "".join(final_text)
thinking = "".join(final_thinking)
# إذا لم يكن هناك محتوى، نستخدم رد افتراضي
if not has_content or not text.strip():
logger.warning("استجابة فارغة، استخدام رد افتراضي")
default_responses = [
"مرحباً! كيف يمكنني مساعدتك اليوم؟",
"أهلاً بك! ما الذي تريد معرفته؟",
"مرحباً! أنا هنا للإجابة على أسئلتك."
]
text = random.choice(default_responses)
tool_calls = None
if has_tools:
tool_calls, text = detect_and_parse_tool_calls(text)
message = {"role": "assistant", "content": text}
if thinking:
message["reasoning_content"] = thinking
if tool_calls:
message["tool_calls"] = tool_calls
reasoning_tokens = estimate_tokens(thinking)
completion_tokens = estimate_tokens(text)
response = {
"id": completion_id,
"object": "chat.completion",
"created": created,
"model": model,
"choices": [{"index": 0, "message": message, "finish_reason": "tool_calls" if tool_calls else "stop"}],
"usage": {
"prompt_tokens": estimate_tokens(final_prompt),
"completion_tokens": reasoning_tokens + completion_tokens,
"total_tokens": estimate_tokens(final_prompt) + reasoning_tokens + completion_tokens,
"completion_tokens_details": {"reasoning_tokens": reasoning_tokens}
}
}
delete_session(request, session_id)
return JSONResponse(content=response)
except HTTPException as exc:
if retry_count < max_retries and "401" in str(exc.detail):
retry_count += 1
continue
return safe_json_response_error(exc.status_code, str(exc.detail))
except Exception as exc:
logger.exception("chat_completions error")
if retry_count < max_retries:
retry_count += 1
continue
return safe_json_response_error(500, "Internal Server Error")
return safe_json_response_error(500, "فشل بعد عدة محاولات")
# ============================================================
# Claude endpoints (shortened)
# ============================================================
@app.get("/anthropic/v1/models")
async def list_claude_models():
return {
"object": "list",
"data": [
{"id": "claude-sonnet-4-20250514", "object": "model", "created": 1715635200, "owned_by": "anthropic"},
{"id": "claude-sonnet-4-20250514-fast", "object": "model", "created": 1715635200, "owned_by": "anthropic"},
{"id": "claude-sonnet-4-20250514-slow", "object": "model", "created": 1715635200, "owned_by": "anthropic"}
]
}
def convert_claude_messages(messages, system=None):
result = []
if system:
if isinstance(system, list):
system_text = "\n".join(normalize_message_content(block) for block in system)
else:
system_text = str(system)
result.append({"role": "system", "content": system_text})
for msg in messages:
role = msg.get("role", "user")
content = msg.get("content", "")
result.append({"role": role, "content": normalize_message_content(content)})
return result
def get_claude_deepseek_model(model):
lower = model.lower()
if "opus" in lower or "slow" in lower or "reasoner" in lower:
return "deepseek-reasoner"
return "deepseek-chat"
def create_deepseek_request_for_claude(request_data):
model = request_data.get("model", CLAUDE_DEFAULT_MODEL)
messages = request_data.get("messages", [])
system = request_data.get("system")
deepseek_messages = convert_claude_messages(messages, system)
tools = request_data.get("tools") or []
if tools:
tool_descriptions = []
for tool in tools:
name = tool.get("name", "unknown")
description = tool.get("description", "")
schema = tool.get("input_schema", {})
tool_descriptions.append(
f"Tool: {name}\nDescription: {description}\nInput schema: {json.dumps(schema, ensure_ascii=False)}"
)
instruction = f"""
You have access to these tools:
{chr(10).join(tool_descriptions)}
If a tool is required, output ONLY:
{{"tool_calls":[{{"name":"tool_name","input":{{"parameter":"value"}}}}]}}
You may call multiple tools in one response.
"""
system_index = None
for index, msg in enumerate(deepseek_messages):
if msg.get("role") == "system":
system_index = index
break
if system_index is None:
deepseek_messages.insert(0, {"role": "system", "content": instruction})
else:
deepseek_messages[system_index]["content"] += "\n\n" + instruction
deepseek_model = get_claude_deepseek_model(model)
return {
"model": deepseek_model,
"messages": deepseek_messages,
"stream": request_data.get("stream", False),
"tools": tools
}
@app.post("/anthropic/v1/messages")
async def claude_messages(request: Request):
session_id = None
deepseek_response = None
try:
determine_claude_mode_and_token(request)
request_data = await request.json()
model = request_data.get("model")
messages = request_data.get("messages", [])
if not model or not messages:
return JSONResponse(
status_code=400,
content={"error": {"type": "invalid_request_error", "message": "Request must include model and messages."}}
)
converted = create_deepseek_request_for_claude(request_data)
deepseek_model = converted["model"]
deepseek_messages = converted["messages"]
tools = converted["tools"]
has_tools = bool(tools)
thinking_enabled = (deepseek_model == "deepseek-reasoner")
final_prompt = messages_prepare(deepseek_messages)
session_id = create_session(request)
if not session_id:
raise HTTPException(status_code=401, detail="Unable to create DeepSeek session.")
pow_response = get_pow_response(request)
if not pow_response:
raise HTTPException(status_code=401, detail="Unable to obtain DeepSeek PoW.")
headers = {**get_auth_headers(request), "x-ds-pow-response": pow_response}
payload = {
"chat_session_id": session_id,
"parent_message_id": None,
"prompt": final_prompt,
"ref_file_ids": [],
"thinking_enabled": thinking_enabled,
"search_enabled": False
}
deepseek_response = call_completion_endpoint(payload, headers, max_attempts=3)
if not deepseek_response:
raise HTTPException(status_code=500, detail="Failed to get Claude response.")
created = int(time.time())
if request_data.get("stream", False):
def claude_stream():
full_text = []
full_thinking = []
try:
message_id = f"msg_{created}_{random.randint(1000,9999)}"
input_tokens = estimate_tokens(final_prompt)
yield f"data: {json.dumps({'type': 'message_start', 'message': {'id': message_id, 'type': 'message', 'role': 'assistant', 'model': model, 'content': [], 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': input_tokens, 'output_tokens': 0}}}, ensure_ascii=False)}\n\n"
yield f"data: {json.dumps({'type': 'content_block_start', 'index': 0, 'content_block': {'type': 'text', 'text': ''}}, ensure_ascii=False)}\n\n"
current_type = "text"
for raw_line in deepseek_response.iter_lines():
if not raw_line:
continue
try:
line = raw_line.decode("utf-8")
except Exception:
continue
if not line.startswith("data:"):
continue
data_str = line[5:].strip()
if data_str == "[DONE]":
break
try:
chunk = json.loads(data_str)
except Exception:
continue
if "v" not in chunk:
continue
value = chunk["v"]
path = chunk.get("p", "")
if path == "response/status":
if value == "FINISHED":
break
continue
if path == "response/search_status":
continue
if path == "response/thinking_content":
current_type = "thinking"
elif path == "response/content":
current_type = "text"
if not isinstance(value, str):
continue
if current_type == "thinking":
full_thinking.append(value)
else:
full_text.append(value)
yield f"data: {json.dumps({'type': 'content_block_delta', 'index': 0, 'delta': {'type': 'text_delta', 'text': value}}, ensure_ascii=False)}\n\n"
yield f"data: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
text = "".join(full_text)
tool_calls = None
if has_tools:
tool_calls, text = detect_and_parse_tool_calls(text)
if tool_calls:
for idx, call in enumerate(tool_calls):
func = call["function"]
try:
tool_input = json.loads(func.get("arguments", "{}"))
except Exception:
tool_input = {}
tool_use_id = f"toolu_{created}_{random.randint(1000,9999)}_{idx}"
yield f"data: {json.dumps({'type': 'content_block_start', 'index': idx+1, 'content_block': {'type': 'tool_use', 'id': tool_use_id, 'name': func['name'], 'input': tool_input}}, ensure_ascii=False)}\n\n"
yield f"data: {json.dumps({'type': 'content_block_stop', 'index': idx+1})}\n\n"
stop_reason = "tool_use" if tool_calls else "end_turn"
output_tokens = estimate_tokens(text)
yield f"data: {json.dumps({'type': 'message_delta', 'delta': {'stop_reason': stop_reason, 'stop_sequence': None}, 'usage': {'output_tokens': output_tokens}}, ensure_ascii=False)}\n\n"
yield f"data: {json.dumps({'type': 'message_stop'})}\n\n"
except Exception as exc:
logger.exception("Claude stream error")
yield f"data: {json.dumps({'type': 'error', 'error': {'type': 'api_error', 'message': str(exc)}})}\n\n"
finally:
try:
deepseek_response.close()
except Exception:
pass
delete_session(request, session_id)
return StreamingResponse(
claude_stream(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"}
)
final_text = []
final_thinking = []
parse_deepseek_stream(deepseek_response, on_text=final_text.append, on_thinking=final_thinking.append)
text = "".join(final_text)
thinking = "".join(final_thinking)
tool_calls = None
if has_tools:
tool_calls, text = detect_and_parse_tool_calls(text)
content = []
if thinking:
content.append({"type": "thinking", "thinking": thinking})
if tool_calls:
for idx, call in enumerate(tool_calls):
func = call["function"]
try:
tool_input = json.loads(func.get("arguments", "{}"))
except Exception:
tool_input = {}
content.append({
"type": "tool_use",
"id": f"toolu_{int(time.time())}_{random.randint(1000,9999)}_{idx}",
"name": func["name"],
"input": tool_input
})
elif text:
content.append({"type": "text", "text": text})
else:
content.append({"type": "text", "text": "No response generated."})
response = {
"id": f"msg_{int(time.time())}_{random.randint(1000,9999)}",
"type": "message",
"role": "assistant",
"model": model,
"content": content,
"stop_reason": "tool_use" if tool_calls else "end_turn",
"stop_sequence": None,
"usage": {
"input_tokens": estimate_tokens(final_prompt),
"output_tokens": estimate_tokens(text) + estimate_tokens(thinking)
}
}
delete_session(request, session_id)
return JSONResponse(content=response)
except HTTPException as exc:
if deepseek_response:
try:
deepseek_response.close()
except Exception:
pass
if session_id:
delete_session(request, session_id)
return JSONResponse(
status_code=exc.status_code,
content={"error": {"type": "invalid_request_error", "message": str(exc.detail)}}
)
except Exception as exc:
logger.exception("Claude messages error")
if deepseek_response:
try:
deepseek_response.close()
except Exception:
pass
if session_id:
delete_session(request, session_id)
return JSONResponse(
status_code=500,
content={"error": {"type": "api_error", "message": "Internal Server Error"}}
)
@app.post("/anthropic/v1/messages/count_tokens")
async def claude_count_tokens(request: Request):
try:
determine_claude_mode_and_token(request)
data = await request.json()
messages = data.get("messages", [])
system = data.get("system", "")
tools = data.get("tools", [])
total = estimate_tokens(system) + estimate_tokens(messages) + estimate_tokens(tools)
return {"input_tokens": max(1, total)}
except HTTPException as exc:
return JSONResponse(
status_code=exc.status_code,
content={"error": {"type": "invalid_request_error", "message": str(exc.detail)}}
)
except Exception:
logger.exception("count_tokens error")
return JSONResponse(
status_code=500,
content={"error": {"type": "api_error", "message": "Internal Server Error"}}
)
# ============================================================
# Stop stream endpoints
# ============================================================
@app.post("/v1/chat/stop_stream")
async def stop_stream(request: Request):
try:
determine_mode_and_token(request)
body = await request.json()
session_id = body.get("chat_session_id")
message_id = body.get("message_id")
if not session_id:
raise HTTPException(status_code=400, detail="Missing chat_session_id.")
response = requests.post(
DEEPSEEK_STOP_STREAM_URL,
headers=get_auth_headers(request),
json={"chat_session_id": session_id, "message_id": message_id},
impersonate="safari15_3",
timeout=15
)
try:
data = response.json()
except Exception:
data = {"success": response.status_code == 200, "message": response.text}
status = response.status_code
response.close()
return JSONResponse(status_code=status, content=data)
except HTTPException:
raise
except Exception as exc:
logger.exception("stop_stream error")
raise HTTPException(status_code=500, detail=str(exc))
@app.post("/anthropic/v1/messages/stop_stream")
async def claude_stop_stream(request: Request):
return await stop_stream(request)
# ============================================================
# Health check
# ============================================================
@app.get("/health")
async def health():
return {
"status": "ok",
"accounts": len(ACCOUNTS),
"wasm_exists": os.path.isfile(WASM_PATH),
"wasm_path": os.path.abspath(WASM_PATH),
"token_cached": len(token_cache)
}
# ============================================================
# Root
# ============================================================
@app.get("/")
async def index(request: Request):
template_path = os.path.join("templates", "welcome.html")
if os.path.isfile(template_path):
return templates.TemplateResponse("welcome.html", {"request": request})
return {
"name": "DeepSeek to API",
"status": "running",
"openai_endpoint": "/v1/chat/completions",
"anthropic_endpoint": "/anthropic/v1/messages",
"models_endpoint": "/v1/models",
"health_endpoint": "/health"
}
# ============================================================
# Run
# ============================================================
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host=HOST, port=PORT)