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(" 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)