commit f41d64f39b2136f122956580668dfc71dc93ad39 Author: whattfkk Date: Thu May 14 18:38:09 2026 +0300 initial commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..91c6f4d --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Environment configuration example +# Copy this file to .env and update with your actual values + +# Redis connection address +REDIS_ADDR=localhost:6379 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d00fedd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["python", "main.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..38ddc09 --- /dev/null +++ b/README.md @@ -0,0 +1,224 @@ +# Python DeepSeek R1 API + +A Python port of the DeepSeek R1 API wrapper (OpenAI-compatible). + +## Architecture + +``` +python/ +├── api/ # DeepSeek API client +│ ├── client.py # Main HTTP client +│ ├── models.py # Data models and response types +│ └── utils.py # Utilities +├── dto/ # Data transfer objects +│ ├── models.py # Request/response models +│ └── utils.py # Utilities +├── kv/ # Cache/KV storage +│ └── cache.py # Redis cache implementation +├── solver/ # WASM proof-of-work solver +│ └── instance.py # Solver wrapper +├── application/ # Flask application +│ └── app.py # Main Flask app with routes +├── main.py # Entry point +└── requirements.txt # Python dependencies +``` + +## Requirements + +- Python 3.8+ +- Redis server (for chat session caching) +- WASM binary from Go implementation (`sha3_wasm_bg.7b9ca65ddd.wasm`) + +## Installation + +### Using pip + +```bash +pip install -r requirements.txt +``` + +### Using Docker + +```bash +docker-compose up +``` + +## Configuration + +Create a `.env` file from the template: + +```bash +cp .env.example .env +``` + +Edit `.env` with your settings: + +``` +REDIS_ADDR=localhost:6379 +``` + +## Running + +### Local Python + +```bash +python main.py +``` + +### Docker + +```bash +docker-compose up +``` + +The server will start on `http://localhost:8080` + +## API Endpoints + +- `GET /` - Health check (returns "started") +- `GET /models` - List available models (returns r1 model) +- `POST /chat/completions` - OpenAI-compatible chat completions + +## API Usage + +### Non-streaming Request + +```bash +curl -X POST http://localhost:8080/chat/completions \ + -H "Authorization: Bearer YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "r1", + "messages": [ + {"role": "user", "content": "Hello!"} + ], + "stream": false + }' +``` + +### Streaming Request + +```bash +curl -X POST http://localhost:8080/chat/completions \ + -H "Authorization: Bearer YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "r1", + "messages": [ + {"role": "user", "content": "Hello!"} + ], + "stream": true + }' +``` + +### Python Example + +```python +import requests + +headers = { + "Authorization": "Bearer YOUR_API_KEY", + "Content-Type": "application/json" +} + +data = { + "model": "r1", + "messages": [ + {"role": "user", "content": "What is Python?"} + ], + "stream": False +} + +response = requests.post( + "http://localhost:8080/chat/completions", + json=data, + headers=headers +) + +print(response.json()) +``` + +## Key Components + +### API Client (`api/client.py`) + +The main HTTP client that communicates with DeepSeek's API servers. Features: +- Chat creation and management +- Message completion with streaming +- Proof-of-Work (PoW) challenge handling +- Authentication and authorization +- Server-Sent Events (SSE) parsing for streaming responses + +### Cache (`kv/cache.py`) + +Redis-based caching system for: +- Chat session persistence +- Message ID tracking +- FNV-1a hash-based key generation + +### WASM Solver (`solver/instance.py`) + +Wraps the WASM SHA3 proof-of-work solver: +- Memory management via Wasmtime +- Hash calculation for PoW challenges +- Compatibility with Go WASM binary + +### Flask Application (`application/app.py`) + +Web framework providing: +- REST API endpoints +- Request/response handling +- Streaming support +- Error handling and logging + +## Design Differences from Go + +1. **HTTP Client**: Uses `requests` library instead of Go's `net/http` +2. **Concurrency**: Python's threading vs Go's goroutines +3. **JSON Serialization**: `json` module instead of `sonic` (Go's fast JSON library) +4. **Logging**: Python's `logging` instead of `zap` +5. **Server**: Flask instead of Echo web framework +6. **WASM Runtime**: `wasmtime-py` instead of `wasmtime-go` + +## Troubleshooting + +### WASM Binary Not Found + +Make sure the WASM binary file exists at: +``` +deepseek4free/pkg/solver/sha3_wasm_bg.7b9ca65ddd.wasm +``` + +### Redis Connection Error + +Ensure Redis is running: +```bash +redis-server +# or with Docker +docker run -d -p 6379:6379 redis:7-alpine +``` + +### Import Errors + +Make sure you're running from the python directory and have set PYTHONPATH: +```bash +export PYTHONPATH=/path/to/python:$PYTHONPATH +python main.py +``` + +## Performance Notes + +- Python is generally slower than Go for this workload +- For production use, consider using Gunicorn: + ```bash + pip install gunicorn + gunicorn -w 4 -b 0.0.0.0:8080 "application.app:Application(solver, cache).app" + ``` + +- WASM solver performance is comparable between Go and Python +- Network I/O is the primary bottleneck + +## License + +Same as the original Go implementation + diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..f23aa39 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +# Python DeepSeek R1 API Implementation diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..77b62e4 --- /dev/null +++ b/api/__init__.py @@ -0,0 +1,3 @@ +from .client import Client + +__all__ = ['Client'] diff --git a/api/__pycache__/__init__.cpython-311.pyc b/api/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..261c24f Binary files /dev/null and b/api/__pycache__/__init__.cpython-311.pyc differ diff --git a/api/__pycache__/client.cpython-311.pyc b/api/__pycache__/client.cpython-311.pyc new file mode 100644 index 0000000..81fb0ac Binary files /dev/null and b/api/__pycache__/client.cpython-311.pyc differ diff --git a/api/__pycache__/models.cpython-311.pyc b/api/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000..e7cc3ea Binary files /dev/null and b/api/__pycache__/models.cpython-311.pyc differ diff --git a/api/client.py b/api/client.py new file mode 100644 index 0000000..360b31a --- /dev/null +++ b/api/client.py @@ -0,0 +1,358 @@ +import json +import gzip +import io +import requests +import base64 +from typing import Optional, Dict, Any, Callable +from .models import PowChallenge, CompletionData + + +class Client: + # API endpoints + AUTH_URL = "https://chat.deepseek.com/api/v0/users/login" + CHAT_CREATE_URL = "https://chat.deepseek.com/api/v0/chat_session/create" + CHAT_DELETE_URL = "https://chat.deepseek.com/api/v0/chat_session/delete" + CHAT_EDIT_URL = "https://chat.deepseek.com/api/v0/chat_session/update_title" + CHAT_LIST_URL = "https://chat.deepseek.com/api/v0/chat_session/fetch_page" + COMPLETION_URL = "https://chat.deepseek.com/api/v0/chat/completion" + HISTORY_BASE_URL = "https://chat.deepseek.com/api/v0/chat/history_messages?chat_session_id=" + LOGOUT_URL = "https://chat.deepseek.com/api/v0/users/logout" + POW_URL = "https://chat.deepseek.com/api/v0/chat/create_pow_challenge" + PROFILE_URL = "https://chat.deepseek.com/api/v0/users/current" + QUOTA_URL = "https://chat.deepseek.com/api/v0/users/feature_quota" + + CHAT_CREATE_BODY = '{"agent":"chat"}' + + def __init__(self, pow_solver, api_key: str): + self.api_key = api_key + self.pow_solver = pow_solver + self.http_client = requests.Session() + # Configure TLS with modern user agent + self.http_client.headers.update({ + 'User-Agent': 'DeepSeek/2.0.0 Android/31', # Updated version + }) + + def _apply_headers(self, req: requests.Request, body_len: int = 0) -> None: + """Apply standard headers to request""" + if 'User-Agent' not in req.headers: + req.headers['User-Agent'] = "DeepSeek/2.0.0 Android/31" + req.headers['Content-Type'] = "application/json" + if body_len > 0: + req.headers['Content-Length'] = str(body_len) + if self.api_key: + req.headers['Authorization'] = f"Bearer {self.api_key}" + req.headers['Accept'] = "application/json" + req.headers['Accept-Encoding'] = "gzip" + req.headers['Accept-Charset'] = "UTF-8" + req.headers['X-Client-Locale'] = "en_US" + req.headers['X-Client-Version'] = "2.0.0" + req.headers['X-Client-Platform'] = "android" + + def _apply_pow_header(self, req: requests.Request, answer: int, pow: PowChallenge) -> None: + """Apply Proof-of-Work header to request""" + header = { + "algorithm": pow.algorithm, + "challenge": pow.challenge, + "salt": pow.salt, + "signature": pow.signature, + "answer": answer, + "target_path": pow.target_path, + } + encoded_header = base64.b64encode(json.dumps(header).encode()).decode() + req.headers['X-Ds-Pow-Response'] = encoded_header + + def _unmarshal(self, body: bytes) -> Dict[str, Any]: + """Parse gzipped JSON response""" + try: + decompressed = gzip.decompress(body) + except: + decompressed = body + return json.loads(decompressed.decode('utf-8')) + + def _execute(self, url: str, body: str, method: str) -> Dict[str, Any]: + """Send a request""" + if body and method != "GET": + req = requests.Request(method, url, data=body) + else: + req = requests.Request(method, url) + + prepared = self.http_client.prepare_request(req) + self._apply_headers(prepared, len(body) if body else 0) + + print(f"[DEBUG] Sending {method} to {url}") + resp = self.http_client.send(prepared) + print(f"[DEBUG] Response status: {resp.status_code}") + print(f"[DEBUG] Response headers: {resp.headers}") + + # Check HTTP status code + if resp.status_code >= 400: + try: + error_data = self._unmarshal(resp.content) + error_msg = error_data.get('msg', error_data.get('message', 'Unknown error')) + except Exception as parse_err: + error_msg = resp.text or f"HTTP {resp.status_code}" + print(f"[DEBUG] Failed to parse error response: {parse_err}") + print(f"[DEBUG] API Error: {error_msg}") + raise Exception(f"API Error ({resp.status_code}): {error_msg}") + + result = self._unmarshal(resp.content) + print(f"[DEBUG] Response data: {result}") + + # Check for API-level error codes in the response + if result and isinstance(result, dict): + code = result.get('code') + if code and code != 0: # 0 or missing code = success + msg = result.get('msg', 'Unknown error') + print(f"[DEBUG] API returned error code {code}: {msg}") + raise Exception(f"API Error ({code}): {msg}") + + return result + + def create_chat(self) -> str: + """Create a new chat session. Returns UUID of a new chat session.""" + data = self._execute(self.CHAT_CREATE_URL, self.CHAT_CREATE_BODY, "POST") + try: + if not data or not data.get('data'): + raise Exception(f"Invalid response structure - missing data field. Response: {data}") + + biz_data = data['data'].get('biz_data') + if not biz_data: + raise Exception(f"Invalid response structure - missing biz_data. Response: {data}") + + # Try both possible structures + if 'chat_session' in biz_data: + return biz_data['chat_session']['id'] + elif 'id' in biz_data: + return biz_data['id'] + else: + raise Exception(f"Could not find chat ID in response. biz_data keys: {biz_data.keys()}") + + except (KeyError, TypeError) as e: + raise Exception(f"Failed to create chat: invalid response structure - {str(e)}. Response: {data}") + + def get_all_chats(self) -> list: + """Get all chat sessions""" + data = self._execute(self.CHAT_LIST_URL, "", "GET") + return data['data']['biz_data']['chat_sessions'] + + def change_title(self, chat_session_id: str, title: str) -> None: + """Change chat session title""" + body = json.dumps({ + "chat_session_id": chat_session_id, + "title": title, + }) + self._execute(self.CHAT_EDIT_URL, body, "POST") + + def delete_chat_session(self, chat_session_id: str) -> None: + """Delete a chat session""" + body = json.dumps({ + "chat_session_id": chat_session_id, + }) + self._execute(self.CHAT_DELETE_URL, body, "POST") + + def get_message_history(self, chat_session_id: str) -> Dict[str, Any]: + """Get message history for a chat session""" + data = self._execute(self.HISTORY_BASE_URL + chat_session_id, "", "GET") + return data['data']['biz_data'] + + def login(self, email: str, password: str, device_id: str) -> str: + """Login and get API token""" + body = json.dumps({ + "email": email, + "password": password, + "device_id": device_id, + "os": "android", + }) + data = self._execute(self.AUTH_URL, body, "POST") + return data['data']['biz_data']['user']['token'] + + def logout(self) -> None: + """Logout""" + self._execute(self.LOGOUT_URL, "", "POST") + + def get_profile(self) -> Dict[str, Any]: + """Get user profile""" + data = self._execute(self.PROFILE_URL, "", "GET") + return data['data']['biz_data'] + + def get_quota(self) -> Dict[str, Any]: + """Get thinking quota""" + data = self._execute(self.QUOTA_URL, "", "GET") + return data['data']['biz_data']['thinking'] + + def _get_pow(self, endpoint: str) -> PowChallenge: + """Get Proof-of-Work challenge""" + body = json.dumps({ + "target_path": endpoint, + }) + data = self._execute(self.POW_URL, body, "POST") + challenge = data['data']['biz_data']['challenge'] + return PowChallenge( + algorithm=challenge.get('algorithm', ''), + challenge=challenge.get('challenge', ''), + salt=challenge.get('salt', ''), + signature=challenge.get('signature', ''), + target_path=challenge.get('target_path', ''), + expire_at=challenge.get('expire_at', 0), + ) + + def completion( + self, + chat_session_id: str, + parent_message: str, + prompt: str, + think: bool, + search: bool, + response_callback: Callable[[str], None], + ) -> None: + """Send completion request and stream responses""" + print(f"[DEBUG] Starting completion for chat {chat_session_id}") + think = False + search = False + + pow = self._get_pow("/api/v0/chat/completion") + print(f"[DEBUG] Got PoW challenge") + + answer = self.pow_solver.calculate_hash( + pow.challenge, + pow.salt, + pow.expire_at, + pow.expire_at, + ) + print(f"[DEBUG] Calculated PoW answer: {answer}") + + completion_data = CompletionData( + chat_session_id=chat_session_id, + prompt=prompt, + thinking_enabled=think, + search_enabled=search, + parent_message_id=None, + ref_file_ids=[], + ) + + if parent_message: + try: + completion_data.parent_message_id = int(parent_message) + except ValueError: + pass + + body = json.dumps(completion_data.to_dict()) + print(f"[DEBUG] Completion request body: {body}") + + req = requests.Request("POST", self.COMPLETION_URL, data=body) + prepared = self.http_client.prepare_request(req) + self._apply_headers(prepared, len(body)) + self._apply_pow_header(prepared, int(answer), pow) + + print(f"[DEBUG] Sending completion request to {self.COMPLETION_URL}") + print(f"[DEBUG] thinking_enabled={think}, search_enabled={search}") + resp = self.http_client.send(prepared, stream=True) + + print(f"[DEBUG] Completion response status: {resp.status_code}") + print(f"[DEBUG] Completion response headers: {dict(resp.headers)}") + + if resp.status_code != 200: + try: + content = resp.content.decode('utf-8') + except: + content = str(resp.content) + print(f"[DEBUG] Error response: {content}") + raise Exception(f"Completion failed with status {resp.status_code}: {content}") + + self._parse_events(resp, response_callback) + + def _parse_events(self, resp, response_callback: Callable[[str], None]) -> None: + """Parse SSE events from response""" + print(f"[DEBUG] Starting to parse events") + line_count = 0 + events_received = [] + think = False + search = False + raw_events_logged = 0 + + def emit_value(value: Any) -> None: + if isinstance(value, str): + if value == "FINISHED": + return + response_callback(value) + return + + if isinstance(value, list): + for item in value: + emit_value(item) + return + + if isinstance(value, dict): + if 'content' in value and isinstance(value['content'], str): + response_callback(value['content']) + for nested_value in value.values(): + if isinstance(nested_value, (dict, list)): + emit_value(nested_value) + + try: + for line in resp.iter_lines(): + line_count += 1 + if not line: + continue + + line = line.decode('utf-8').strip() if isinstance(line, bytes) else line.strip() + + if line.startswith("event: "): + continue + + raw = line[6:] if line.startswith("data: ") else line + if not raw: + continue + + try: + data = json.loads(raw) + except json.JSONDecodeError as e: + print(f"[DEBUG] Failed to parse JSON on line {line_count}: {e}") + print(f"[DEBUG] Raw line: {raw}") + continue + + if raw_events_logged < 5: + print(f"[DEBUG] Raw event #{raw_events_logged + 1}: {raw}") + raw_events_logged += 1 + + p = data.get('p', '') + v = data.get('v') + + events_received.append(p) + print(f"[DEBUG] Parsed event type: {p}, has value: {v is not None}") + + if p: + if p == "response/search_status": + if not search: + response_callback("\n\n") + search = True + if p == "response/thinking_content": + if not search: + response_callback("\n\n") + response_callback("\n\n") + think = True + elif p == "response/content": + if think: + response_callback("\n\n") + + if isinstance(v, dict): + fragments = v.get('response', {}).get('message', {}).get('fragments', []) + if isinstance(fragments, list) and fragments: + for fragment in fragments: + if isinstance(fragment, dict): + fragment_content = fragment.get('content') + if isinstance(fragment_content, str): + response_callback(fragment_content) + elif fragment_content is not None: + emit_value(fragment_content) + else: + emit_value(v) + else: + emit_value(v) + + print(f"[DEBUG] Finished parsing events. Events received: {events_received}") + except Exception as e: + print(f"[DEBUG] Error in _parse_events: {e}") + print(f"[DEBUG] Line count: {line_count}") + print(f"[DEBUG] Events received so far: {events_received}") + raise diff --git a/api/models.py b/api/models.py new file mode 100644 index 0000000..de5df95 --- /dev/null +++ b/api/models.py @@ -0,0 +1,149 @@ +from dataclasses import dataclass, field +from typing import List, Optional, Dict, Any, Union + + +@dataclass +class SearchResponse: + url: str + title: str + snippet: str + cite_index: Optional[int] = None + published_at: Optional[Any] = None + site_name: Optional[Any] = None + site_icon: str = "" + + +@dataclass +class ChatSession: + id: str + seq_id: int + title: Optional[str] + title_type: Optional[str] + updated_at: float + agent: str + version: int + current_message_id: Optional[int] + inserted_at: float + character: Optional[str] = None + + @staticmethod + def from_dict(data: Dict[str, Any]) -> 'ChatSession': + return ChatSession( + id=data.get('id', ''), + seq_id=data.get('seq_id', 0), + title=data.get('title'), + title_type=data.get('title_type'), + updated_at=data.get('updated_at', 0), + agent=data.get('agent', ''), + version=data.get('version', 0), + current_message_id=data.get('current_message_id'), + inserted_at=data.get('inserted_at', 0), + character=data.get('character'), + ) + + +@dataclass +class ChatMessage: + message_id: int + parent_id: Optional[int] + model: str + role: str + content: str + thinking_enabled: bool + thinking_content: Optional[str] + thinking_elapsed_secs: Optional[int] + ban_edit: bool + ban_regenerate: bool + status: str + accumulated_token_usage: int + files: List[Any] = field(default_factory=list) + tips: List[Any] = field(default_factory=list) + inserted_at: float = 0.0 + search_enabled: bool = False + search_status: Optional[str] = None + search_results: List[SearchResponse] = field(default_factory=list) + + +@dataclass +class ChatHistory: + chat_session: ChatSession + chat_messages: List[ChatMessage] + cache_valid: bool + route_id: Optional[Any] + + +@dataclass +class PowChallenge: + algorithm: str + challenge: str + salt: str + signature: str + target_path: str + expire_at: int = 0 + + +@dataclass +class CompletionData: + chat_session_id: str + prompt: str + thinking_enabled: bool = False + search_enabled: bool = False + parent_message_id: Optional[int] = None + ref_file_ids: List[str] = field(default_factory=list) + + def to_dict(self) -> Dict[str, Any]: + data = { + 'chat_session_id': self.chat_session_id, + 'prompt': self.prompt, + 'thinking_enabled': self.thinking_enabled, + 'search_enabled': self.search_enabled, + 'ref_file_ids': self.ref_file_ids, + } + if self.parent_message_id is not None: + data['parent_message_id'] = self.parent_message_id + else: + data['parent_message_id'] = None + return data + + +# Response wrappers for API +@dataclass +class AuthResponse: + code: int + msg: str + data: Dict[str, Any] + + +@dataclass +class ChatCreateResponse: + code: int + msg: str + data: Dict[str, Any] + + +@dataclass +class ChatEditResponse: + code: int + msg: str + data: Dict[str, Any] + + +@dataclass +class NullResponse: + code: int + msg: str + data: Dict[str, Any] + + +@dataclass +class ProfileResponse: + code: int + msg: str + data: Dict[str, Any] + + +@dataclass +class QuotaResponse: + code: int + msg: str + data: Dict[str, Any] diff --git a/api/utils.py b/api/utils.py new file mode 100644 index 0000000..44e0036 --- /dev/null +++ b/api/utils.py @@ -0,0 +1,3 @@ +""" +API utilities - empty utilities for compatibility +""" diff --git a/application/__init__.py b/application/__init__.py new file mode 100644 index 0000000..2405cc4 --- /dev/null +++ b/application/__init__.py @@ -0,0 +1,3 @@ +from .app import Application + +__all__ = ['Application'] diff --git a/application/__pycache__/__init__.cpython-311.pyc b/application/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..34e4af6 Binary files /dev/null and b/application/__pycache__/__init__.cpython-311.pyc differ diff --git a/application/__pycache__/app.cpython-311.pyc b/application/__pycache__/app.cpython-311.pyc new file mode 100644 index 0000000..265fba4 Binary files /dev/null and b/application/__pycache__/app.cpython-311.pyc differ diff --git a/application/app.py b/application/app.py new file mode 100644 index 0000000..1b896bc --- /dev/null +++ b/application/app.py @@ -0,0 +1,219 @@ +import json +import random +import time +import sys +import traceback +from pathlib import Path +from flask import Flask, request, jsonify, Response, stream_with_context +from typing import Generator + +# Add parent directory to path for imports +parent_dir = Path(__file__).parent.parent +sys.path.insert(0, str(parent_dir)) + +from api import Client +from dto import ChatCompletionRequest, ChatCompletionResponse, Choice, Message, ChunkResponse, ChunkChoice, Delta, Model +from kv import Cache, ChatData +from solver import Solver + + +class Application: + def __init__(self, solver: Solver, cache: Cache): + self.solver = solver + self.cache = cache + self.app = Flask(__name__) + self._setup_routes() + + def _setup_routes(self): + """Setup Flask routes""" + + @self.app.route('/', methods=['GET']) + def health(): + return "started", 200 + + @self.app.route('/models', methods=['GET']) + def models(): + models_list = { + "object": "list", + "data": [ + { + "id": "r1", + "object": "model", + "owned_by": "deepseek", + }, + { + "id": "deepseek-chat", + "object": "model", + "owned_by": "deepseek", + }, + { + "id": "deepseek-reasoner", + "object": "model", + "owned_by": "deepseek", + }, + ], + } + return jsonify(models_list), 200 + + @self.app.route('/chat/completions', methods=['POST']) + def chat(): + return self._handle_chat() + + def _handle_chat(self): + """Handle chat completion request""" + print("[DEBUG] _handle_chat called") + + auth_header = request.headers.get('Authorization', '') + if not auth_header: + print("[DEBUG] No authorization header") + return jsonify({"error": "Authorization header required"}), 401 + + api_key = auth_header.replace('Bearer ', '').strip() + print(f"[DEBUG] API key (first 10 chars): {api_key[:10]}...") + + # Validate API key is not empty + if not api_key: + print("[DEBUG] API key is empty") + return jsonify({"error": "API key cannot be empty. Please provide a valid Bearer token."}), 401 + + try: + data = request.get_json() + print(f"[DEBUG] Request data: {data}") + req = ChatCompletionRequest.from_dict(data) + print(f"[DEBUG] Parsed request: model={req.model}, stream={req.stream}, messages={len(req.messages)}, thinking_enabled={req.thinking_enabled}, search_enabled={req.search_enabled}") + except Exception as e: + print(f"[DEBUG] Failed to parse request: {e}") + return jsonify({"error": str(e)}), 400 + + print("[DEBUG] Creating API client") + api_client = Client(self.solver, api_key) + + try: + print("[DEBUG] Getting chat data from cache") + chat_data = self.cache.get_chat_data(api_key, req.messages[0].content) + print(f"[DEBUG] Cache data: chat_id={chat_data.chat_id}, current_msg_id={chat_data.current_message_id}") + + if not chat_data.chat_id: + print("[DEBUG] Creating new chat") + chat_data.chat_id = api_client.create_chat() + print(f"[DEBUG] Created chat: {chat_data.chat_id}") + else: + print("[DEBUG] Using existing chat") + if not chat_data.current_message_id: + chat_data.current_message_id = "0" + msg_id = int(chat_data.current_message_id) + msg_id += 2 + chat_data.current_message_id = str(msg_id) + print(f"[DEBUG] Updated message ID: {chat_data.current_message_id}") + + except Exception as e: + print(f"[DEBUG] Error in chat setup: {e}") + print(f"[DEBUG] Error traceback: {traceback.format_exc()}") + return jsonify({"error": str(e)}), 400 + + def save_and_change_title(): + text = req.messages[0].content + # Special handling for title/follow-up/tags requests + if text.startswith("### Task:\nGenerate a concise, 3-5 word"): + text = f"title_req_{int(time.time())}" + elif text.startswith("### Task:\nSuggest 3-5"): + text = f"follow_req_{int(time.time())}" + elif text.startswith("### Task:\nGenerate 1-3 broad"): + text = f"tags_req_{int(time.time())}" + + try: + api_client.change_title(chat_data.chat_id, text) + self.cache.set_chat_data(api_key, req.messages[0].content, chat_data) + except Exception as e: + print(f"Error saving chat data: {e}") + + # Collect responses in a generator + def response_generator() -> Generator[str, None, None]: + responses = [] + + def collect_response(msg: str): + responses.append(msg) + + try: + print(f"[DEBUG] Calling completion with thinking_enabled={req.thinking_enabled}, search_enabled={req.search_enabled}") + api_client.completion( + chat_data.chat_id, + chat_data.current_message_id, + req.messages[-1].content, + False, + False, + collect_response, + ) + print(f"[DEBUG] Completion finished") + + save_and_change_title() + + if req.stream: + for msg in responses: + chunk = ChunkResponse( + id=f"chatcmpl-{random.randint(0, 1000000)}", + object="chat.completion.chunk", + created=int(time.time()), + model=req.model, + choices=[ + ChunkChoice( + index=0, + delta=Delta(content=msg), + finish_reason=None, + ) + ], + ) + yield f"data: {json.dumps(chunk.to_dict())}\n\n" + time.sleep(random.uniform(0.1, 0.2)) + + yield "data: [DONE]\n\n" + else: + answer = "".join(responses) + response = ChatCompletionResponse( + id=f"chatcmpl-{random.randint(0, 1000000)}", + object="chat.completion", + created=int(time.time()), + model=req.model, + choices=[ + Choice( + index=0, + message=Message(role="assistant", content=answer), + finish_reason="stop", + ) + ], + ) + yield json.dumps(response.to_dict()) + + except Exception as e: + error_tb = traceback.format_exc() + print(f"Error in completion: {e}") + print(error_tb) + if req.stream: + yield f"data: {json.dumps({'error': str(e), 'traceback': error_tb})}\n\n" + else: + yield json.dumps({"error": str(e), "traceback": error_tb}) + + if req.stream: + return Response( + stream_with_context(response_generator()), + mimetype='text/event-stream', + headers={ + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Access-Control-Allow-Origin': '*', + }, + ) + else: + return Response( + response_generator(), + mimetype='application/json', + ) + + def run(self, host: str = "0.0.0.0", port: int = 8080, debug: bool = False): + """Run the Flask application""" + self.app.run(host=host, port=port, debug=debug, threaded=True) + + def close(self): + """Close the application""" + self.cache.close() + self.solver.close() diff --git a/config.py b/config.py new file mode 100644 index 0000000..31823d4 --- /dev/null +++ b/config.py @@ -0,0 +1,6 @@ +import sys +from pathlib import Path + +# Add the python directory to sys.path for imports +python_dir = Path(__file__).parent +sys.path.insert(0, str(python_dir)) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..10d6b34 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.8' + +services: + redis: + image: redis:7-alpine + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + + app: + build: . + ports: + - "8080:8080" + environment: + - REDIS_ADDR=redis:6379 + depends_on: + - redis + volumes: + - ./:/app + +volumes: + redis_data: diff --git a/dto/__init__.py b/dto/__init__.py new file mode 100644 index 0000000..dbb7cc1 --- /dev/null +++ b/dto/__init__.py @@ -0,0 +1,12 @@ +from .models import ChatCompletionRequest, Message, ChatCompletionResponse, Choice, ChunkResponse, ChunkChoice, Delta, Model + +__all__ = [ + 'ChatCompletionRequest', + 'Message', + 'ChatCompletionResponse', + 'Choice', + 'ChunkResponse', + 'ChunkChoice', + 'Delta', + 'Model', +] diff --git a/dto/__pycache__/__init__.cpython-311.pyc b/dto/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..417d96a Binary files /dev/null and b/dto/__pycache__/__init__.cpython-311.pyc differ diff --git a/dto/__pycache__/models.cpython-311.pyc b/dto/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000..dc2c307 Binary files /dev/null and b/dto/__pycache__/models.cpython-311.pyc differ diff --git a/dto/models.py b/dto/models.py new file mode 100644 index 0000000..d235b6a --- /dev/null +++ b/dto/models.py @@ -0,0 +1,137 @@ +from dataclasses import dataclass, field +from typing import List, Optional, Dict, Any + + +@dataclass +class Message: + role: str + content: str + + def to_dict(self) -> Dict[str, str]: + return { + 'role': self.role, + 'content': self.content, + } + + @staticmethod + def from_dict(data: Dict[str, str]) -> 'Message': + return Message(role=data['role'], content=data['content']) + + +@dataclass +class Delta: + role: Optional[str] = None + content: Optional[str] = None + + def to_dict(self) -> Dict[str, Optional[str]]: + result = {} + if self.role is not None: + result['role'] = self.role + if self.content is not None: + result['content'] = self.content + return result + + +@dataclass +class Choice: + index: int + message: Message + finish_reason: str + + def to_dict(self) -> Dict[str, Any]: + return { + 'index': self.index, + 'message': self.message.to_dict(), + 'finish_reason': self.finish_reason, + } + + +@dataclass +class ChatCompletionResponse: + id: str + object: str + created: int + model: str + choices: List[Choice] = field(default_factory=list) + + def to_dict(self) -> Dict[str, Any]: + return { + 'id': self.id, + 'object': self.object, + 'created': self.created, + 'model': self.model, + 'choices': [c.to_dict() for c in self.choices], + } + + +@dataclass +class ChunkChoice: + index: int + delta: Delta + finish_reason: Optional[Any] = None + + def to_dict(self) -> Dict[str, Any]: + return { + 'index': self.index, + 'delta': self.delta.to_dict(), + 'finish_reason': self.finish_reason, + } + + +@dataclass +class ChunkResponse: + id: str + object: str + created: int + model: str + choices: List[ChunkChoice] = field(default_factory=list) + + def to_dict(self) -> Dict[str, Any]: + return { + 'id': self.id, + 'object': self.object, + 'created': self.created, + 'model': self.model, + 'choices': [c.to_dict() for c in self.choices], + } + + +@dataclass +class ChatCompletionRequest: + model: str + messages: List[Message] + stream: bool = False + return_images: bool = False + temperature: float = 0.0 + web_search_options: Optional[Dict[str, str]] = None + thinking_enabled: bool = False # Enable reasoning by default + search_enabled: bool = False # Enable search by default + + @staticmethod + def from_dict(data: Dict[str, Any]) -> 'ChatCompletionRequest': + messages = [Message.from_dict(m) for m in data.get('messages', [])] + web_search_options = data.get('web_search_options', {}) + return ChatCompletionRequest( + model=data.get('model', 'r1'), + messages=messages, + stream=data.get('stream', False), + return_images=data.get('return_images', False), + temperature=data.get('temperature', 0.0), + web_search_options=web_search_options, + thinking_enabled=False, + search_enabled=False, + ) + + +@dataclass +class Model: + id: str + object: str + owned_by: str + + def to_dict(self) -> Dict[str, str]: + return { + 'id': self.id, + 'object': self.object, + 'owned_by': self.owned_by, + } diff --git a/dto/utils.py b/dto/utils.py new file mode 100644 index 0000000..512b5d8 --- /dev/null +++ b/dto/utils.py @@ -0,0 +1,3 @@ +""" +Utilities module - empty utilities for compatibility +""" diff --git a/kv/__init__.py b/kv/__init__.py new file mode 100644 index 0000000..f7ed475 --- /dev/null +++ b/kv/__init__.py @@ -0,0 +1,3 @@ +from .cache import Cache, ChatData + +__all__ = ['Cache', 'ChatData'] diff --git a/kv/__pycache__/__init__.cpython-311.pyc b/kv/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..dbe6f9b Binary files /dev/null and b/kv/__pycache__/__init__.cpython-311.pyc differ diff --git a/kv/__pycache__/cache.cpython-311.pyc b/kv/__pycache__/cache.cpython-311.pyc new file mode 100644 index 0000000..cfa9bd8 Binary files /dev/null and b/kv/__pycache__/cache.cpython-311.pyc differ diff --git a/kv/cache.py b/kv/cache.py new file mode 100644 index 0000000..83fb0ba --- /dev/null +++ b/kv/cache.py @@ -0,0 +1,64 @@ +import redis +import hashlib +import json +from typing import Optional, Dict, Any + + +class ChatData: + def __init__(self, chat_id: str = "", current_message_id: str = ""): + self.chat_id = chat_id + self.current_message_id = current_message_id + + def serialize(self) -> str: + """Serialize to string format""" + return f"{self.chat_id};{self.current_message_id}" + + @staticmethod + def deserialize(text: str) -> 'ChatData': + """Deserialize from string format""" + parts = text.split(';') + if len(parts) > 2: + raise ValueError("Invalid cache data") + + chat_id = parts[0] if len(parts) > 0 else "" + current_message_id = parts[1] if len(parts) > 1 else "" + + return ChatData(chat_id=chat_id, current_message_id=current_message_id) + + +class Cache: + def __init__(self, redis_addr: str = "localhost:6379"): + """Initialize cache with Redis connection""" + host, port = redis_addr.split(':') + self.redis = redis.Redis(host=host, port=int(port), decode_responses=True) + # Test connection + self.redis.ping() + + def _get_key(self, token: str, title: str) -> str: + """Generate cache key using FNV-1a hash""" + combined = f"{token};{title}" + # Simple FNV-1a hash implementation + hash_value = 0xcbf29ce484222325 + for byte in combined.encode('utf-8'): + hash_value ^= byte + hash_value = (hash_value * 0x100000001b3) & 0xffffffffffffffff + return str(hash_value) + + def get_chat_data(self, token: str, title: str) -> ChatData: + """Get chat data from cache""" + key = self._get_key(token, title) + data = self.redis.get(key) + + if data is None: + return ChatData(chat_id="", current_message_id="") + + return ChatData.deserialize(data) + + def set_chat_data(self, token: str, title: str, data: ChatData) -> None: + """Set chat data in cache""" + key = self._get_key(token, title) + self.redis.set(key, data.serialize()) + + def close(self): + """Close Redis connection""" + self.redis.close() diff --git a/main.py b/main.py new file mode 100644 index 0000000..04da8cc --- /dev/null +++ b/main.py @@ -0,0 +1,68 @@ +import os +import sys +import signal +import logging +from pathlib import Path +from dotenv import load_dotenv + +# Add the python directory to sys.path +current_dir = Path(__file__).parent +sys.path.insert(0, str(current_dir)) + +from application import Application +from solver import Solver +from kv import Cache + +# Load environment variables +load_dotenv() + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +def main(): + logger.info("Logger initialized") + + # Get Redis address from environment + redis_addr = os.getenv("REDIS_ADDR", "localhost:6379") + + # Initialize cache + try: + cache = Cache(redis_addr) + logger.info("Cache initialized") + except Exception as e: + logger.error(f"Failed to initialize cache: {e}") + raise + + # Initialize WASM solver + try: + solver = Solver() + logger.info("WASM solver initialized") + except Exception as e: + logger.error(f"Failed to initialize WASM solver: {e}") + raise + + # Create application + app = Application(solver, cache) + logger.info("Application initialized") + + def signal_handler(sig, frame): + logger.info("Received signal, shutting down...") + app.close() + logger.info("Application stopped") + exit(0) + + # Handle SIGINT and SIGTERM + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + logger.info("Application started on port 8080") + app.run(host="0.0.0.0", port=8080, debug=False) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8e95e6b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +flask==3.0.0 +redis==5.0.1 +requests==2.31.0 +wasmtime==17.0.0 +python-dotenv==1.0.0 diff --git a/solver/__init__.py b/solver/__init__.py new file mode 100644 index 0000000..a1e39b0 --- /dev/null +++ b/solver/__init__.py @@ -0,0 +1,3 @@ +from .instance import Solver + +__all__ = ['Solver'] diff --git a/solver/__pycache__/__init__.cpython-311.pyc b/solver/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..60d984d Binary files /dev/null and b/solver/__pycache__/__init__.cpython-311.pyc differ diff --git a/solver/__pycache__/instance.cpython-311.pyc b/solver/__pycache__/instance.cpython-311.pyc new file mode 100644 index 0000000..f403f59 Binary files /dev/null and b/solver/__pycache__/instance.cpython-311.pyc differ diff --git a/solver/instance.py b/solver/instance.py new file mode 100644 index 0000000..9875a87 --- /dev/null +++ b/solver/instance.py @@ -0,0 +1,165 @@ +import wasmtime +import struct +import math +import os +import ctypes +from pathlib import Path + + +class Solver: + def __init__(self): + """Initialize the WASM solver""" + # Try to find the WASM file + current_dir = Path(__file__).parent + go_wasm_path = Path(__file__).parent.parent.parent / 'deepseek4free' / 'pkg' / 'solver' / 'sha3_wasm_bg.7b9ca65ddd.wasm' + + if go_wasm_path.exists(): + with open(go_wasm_path, 'rb') as f: + wasm_bytes = f.read() + else: + raise FileNotFoundError(f"WASM file not found at {go_wasm_path}") + + engine = wasmtime.Engine() + self.module = wasmtime.Module(engine, wasm_bytes) + self.store = wasmtime.Store(engine) + self.linker = wasmtime.Linker(engine) + self.linker.define_wasi() + + self.instance = self.linker.instantiate(self.store, self.module) + + # Get exports - handle both old and new wasmtime-py API + exports = self.instance.exports(self.store) + + # Get memory export + try: + # Try direct attribute access first + self.memory = exports.memory + except AttributeError: + # Try dict-like access + try: + self.memory = exports['memory'] + except (KeyError, TypeError): + # Try get_export method + mem_extern = self.instance.get_export(self.store, 'memory') + if mem_extern and hasattr(mem_extern, 'memory'): + self.memory = mem_extern.memory + else: + raise RuntimeError("Could not find memory export in WASM module") + + # Initialize functions - with error handling + try: + self.alloc_fn = exports.__wbindgen_export_0 + except AttributeError: + self.alloc_fn = exports['__wbindgen_export_0'] + + try: + self.stack_ptr_fn = exports.__wbindgen_add_to_stack_pointer + except AttributeError: + self.stack_ptr_fn = exports['__wbindgen_add_to_stack_pointer'] + + try: + self.solve_fn = exports.wasm_solve + except AttributeError: + self.solve_fn = exports['wasm_solve'] + + def _write_to_memory(self, text: str) -> tuple[int, int]: + """Write a string to WASM memory and return pointer and length""" + text_bytes = text.encode('utf-8') + length = len(text_bytes) + + # Allocate memory - pass store as first argument + ptr = self.alloc_fn(self.store, length, 1) + print(f"[DEBUG] Allocated memory at ptr={ptr}, length={length}") + + # Get memory data pointer + mem_ptr = self.memory.data_ptr(self.store) + print(f"[DEBUG] Memory pointer type: {type(mem_ptr)}") + + # Write to memory - mem_ptr is a ctypes pointer, can index it directly + try: + for i, byte in enumerate(text_bytes): + mem_ptr[ptr + i] = byte + print(f"[DEBUG] Successfully wrote {length} bytes to memory") + except TypeError as e: + print(f"[DEBUG] Error writing to memory: {e}") + # Try alternative approach with ctypes address + try: + addr = ctypes.cast(mem_ptr, ctypes.c_void_p).value + print(f"[DEBUG] Memory address: {addr}") + buffer = (ctypes.c_ubyte * length).from_address(addr + ptr) + for i, byte in enumerate(text_bytes): + buffer[i] = byte + print(f"[DEBUG] Successfully wrote {length} bytes using ctypes buffer") + except Exception as e2: + print(f"[DEBUG] Also failed with ctypes: {e2}") + raise + + return ptr, length + + def _read_memory(self, ptr: int, length: int) -> bytes: + """Read bytes from WASM memory""" + mem_ptr = self.memory.data_ptr(self.store) + print(f"[DEBUG] Reading {length} bytes from ptr={ptr}, memory_ptr type={type(mem_ptr)}") + + try: + # Try direct indexing of ctypes pointer + result = [] + for i in range(length): + result.append(mem_ptr[ptr + i]) + return bytes(result) + except TypeError as e: + print(f"[DEBUG] Error reading with direct indexing: {e}") + # Try converting to address and using ctypes + try: + addr = ctypes.cast(mem_ptr, ctypes.c_void_p).value + print(f"[DEBUG] Memory address: {addr}") + buffer = (ctypes.c_ubyte * length).from_address(addr + ptr) + return bytes(buffer) + except Exception as e2: + print(f"[DEBUG] Also failed with ctypes: {e2}") + raise + + def calculate_hash(self, challenge: str, salt: str, difficulty: int, expire_at: int) -> int: + """Calculate hash using WASM solver""" + print(f"[DEBUG] calculate_hash called with challenge={challenge[:20]}..., salt={salt}, difficulty={difficulty}") + prefix = f"{salt}_{expire_at}_" + + # Adjust stack pointer - pass store as first argument + retptr = self.stack_ptr_fn(self.store, -16) + print(f"[DEBUG] Stack pointer adjusted, retptr={retptr}") + + # Write to memory + challenge_ptr, challenge_len = self._write_to_memory(challenge) + prefix_ptr, prefix_len = self._write_to_memory(prefix) + print(f"[DEBUG] Wrote challenge at {challenge_ptr}, prefix at {prefix_ptr}") + + # Call solve function - pass store as first argument + print(f"[DEBUG] Calling solve function with retptr={retptr}, challenge_ptr={challenge_ptr}, difficulty={difficulty}") + self.solve_fn(self.store, retptr, challenge_ptr, challenge_len, prefix_ptr, prefix_len, float(difficulty)) + + # Read result from memory + status_bytes = self._read_memory(retptr, 4) + status = struct.unpack('