From f41d64f39b2136f122956580668dfc71dc93ad39 Mon Sep 17 00:00:00 2001 From: whattfkk Date: Thu, 14 May 2026 18:38:09 +0300 Subject: [PATCH] initial commit --- .env.example | 5 + Dockerfile | 14 + README.md | 224 +++++++++++ __init__.py | 1 + api/__init__.py | 3 + api/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 248 bytes api/__pycache__/client.cpython-311.pyc | Bin 0 -> 20284 bytes api/__pycache__/models.cpython-311.pyc | Bin 0 -> 7551 bytes api/client.py | 358 ++++++++++++++++++ api/models.py | 149 ++++++++ api/utils.py | 3 + application/__init__.py | 3 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 258 bytes application/__pycache__/app.cpython-311.pyc | Bin 0 -> 13016 bytes application/app.py | 219 +++++++++++ config.py | 6 + docker-compose.yml | 24 ++ dto/__init__.py | 12 + dto/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 458 bytes dto/__pycache__/models.cpython-311.pyc | Bin 0 -> 7691 bytes dto/models.py | 137 +++++++ dto/utils.py | 3 + kv/__init__.py | 3 + kv/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 280 bytes kv/__pycache__/cache.cpython-311.pyc | Bin 0 -> 4326 bytes kv/cache.py | 64 ++++ main.py | 68 ++++ requirements.txt | 5 + solver/__init__.py | 3 + solver/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 253 bytes solver/__pycache__/instance.cpython-311.pyc | Bin 0 -> 9635 bytes solver/instance.py | 165 ++++++++ 32 files changed, 1469 insertions(+) create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 __init__.py create mode 100644 api/__init__.py create mode 100644 api/__pycache__/__init__.cpython-311.pyc create mode 100644 api/__pycache__/client.cpython-311.pyc create mode 100644 api/__pycache__/models.cpython-311.pyc create mode 100644 api/client.py create mode 100644 api/models.py create mode 100644 api/utils.py create mode 100644 application/__init__.py create mode 100644 application/__pycache__/__init__.cpython-311.pyc create mode 100644 application/__pycache__/app.cpython-311.pyc create mode 100644 application/app.py create mode 100644 config.py create mode 100644 docker-compose.yml create mode 100644 dto/__init__.py create mode 100644 dto/__pycache__/__init__.cpython-311.pyc create mode 100644 dto/__pycache__/models.cpython-311.pyc create mode 100644 dto/models.py create mode 100644 dto/utils.py create mode 100644 kv/__init__.py create mode 100644 kv/__pycache__/__init__.cpython-311.pyc create mode 100644 kv/__pycache__/cache.cpython-311.pyc create mode 100644 kv/cache.py create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 solver/__init__.py create mode 100644 solver/__pycache__/__init__.cpython-311.pyc create mode 100644 solver/__pycache__/instance.cpython-311.pyc create mode 100644 solver/instance.py 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 0000000000000000000000000000000000000000..261c24f73dcf4af0d774259d9e81a574e73cd914 GIT binary patch literal 248 zcmZ3^%ge<81dA(JvkZasV-N=hn4pZ$5>yag4pasJzra8O literal 0 HcmV?d00001 diff --git a/api/__pycache__/client.cpython-311.pyc b/api/__pycache__/client.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81fb0ac2dcecf30be214955698534396952c00f1 GIT binary patch literal 20284 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 oe|EwK%C- 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 0000000000000000000000000000000000000000..34e4af66df7ae8a4e476c553953bb159ce1111e4 GIT binary patch literal 258 zcmZ3^%ge<81dA(Jvn+u0V-N=hn4pZ$5QkWIho0cC7JnoMa)1kKTYOa%!vgBV9qV}`1r(}ocQ>a44*;f{&Lq3 zElw>e)-TB@N=;46F9kAPQp-|v@(WUn^ixt(3yM=yvvm^-GW828OEU8F^noV9jMtBk z&&pL|Om< literal 0 HcmV?d00001 diff --git a/application/__pycache__/app.cpython-311.pyc b/application/__pycache__/app.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..265fba4610156f08a95a08d1635dda4b42b00a45 GIT binary patch literal 13016 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..417d96a3962986ed5917d979dfedcfb5496c16a5 GIT binary patch literal 458 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dbe6f9b112f0eb02ec5d483dcc219e8e3e924fd7 GIT binary patch literal 280 zcmZ3^%ge<81dA(Jvs{4mV-N=hn4pZ$azMs(h7^Vr#vF!R#wf;IrYI&xh7_h0=5(eg z<`kA-22IwNj6g-2Ot)B_6O%JiZ*e$hB$l`&mLwK21I3G2fP|kW%PrPqkSs#%7JGbr zVopwc{7Qz;KqU;n?DRv6Q;UlAOEQX5Q`7QGflQawvecaXg481Yl+@IM;?&e^-Nb@S z{esGpjQl+P>@xlM_{_Y_lK6PNg34bUHo5sJr8%i~MIe8H+*GU$Bt9@RGBVy^P`ZE( UJ>V9a;B|pZzJVPCi#UMl0p#LG=l}o! literal 0 HcmV?d00001 diff --git a/kv/__pycache__/cache.cpython-311.pyc b/kv/__pycache__/cache.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cfa9bd8faa1ea8c3614aec4d4fd8d21f55a0a7cf GIT binary patch literal 4326 zcmb6cU2GG{dG^oQIK~S}48(B31j1n&h=9A63pCOlxuT<$bK>W8a<8(?&V)GF>vVQQ ziRBzol?Q_&K~W^!yQ{j=NgNa&sM<=X>O)^D_0dL_h*pY(RNVt_LEIBB?f1>vj_uG> zJG=hAnfYeEoo~Khf3syvkU;zTnl>}36Y?({G@4rPyqbc}T|x=G0WD1i=&PNnlC-t__(CgO^xPtH1- zyp_&+>O{tLJmr0B-mJ3`NUINC{R}#H2_twUI8KntOrZ)>rc|mfX-So8(3fdoNlj`j zK)ZmdQ36zbNxcVqAJ!QWlI)`0aEKmHKRumxCelvYBVLyY{Zxi}z2+R}%yLpWX4~n@ z4CYLwxtD;@gy8tq5P-X+C>2SnNiNDTk{Uxw;4@On!C3fme&9cfH~#)-WHz#>`QF<4TUL#spYl+>EOry|~0Kq6j%GiQ!lX7ltg ziANK%?uQP@N@f82?Mtu|7&ljcNq(n*v~nSOp7BgNn<=oElaJXB&sdjZQ+b|CJFlga zJOtYxNPG(*8#l*z7@*e;qWilFy-fj{bGXUg1z?dp@7=yKQZdHhb^B|3Px#%6KE}5q zAJt*koHiNIzJ&~6utEkGCN?)J-2?i=raTLhgS+aj*g>W87352};WP{sMpS9%@$2vB zq1JimMTx2d^)C@(NmN^qzxV5bmYso)or=@dWJSOpl}BCeJ_uUU3xgA^dAqSGp3k*w ziuVDJ7ij8*-jA%SpiUYSwict{E*uFQ&FTf-m(Xck^)!1nn*kTjGRq5olFrVt4>`|s zj^~N1d^;ddn@w}aZn1v6MzH?68T8PNV-@mY>V6t-U*hE|HfD` zO4?daxHNZTY-{$5JUTm`dgHbS{hiP%8DyxpcNpQNSO#0jK8?0A9#;(oV+A zG4N-~2Vv>~5xj*{h>H^dZ_r)_u-JUoWm$W7od9qZfu5KGbpl=1PL_}3?8#1`lR)pf z+^u~qmG`X^#O@&4egRl*Ox-r)cAw58YQ#$Q|d7XxqNV1y-!Is0Gb{eDL>u@%2rnA#| z+Zp@e@gKeC1v#S`JC&xCn`p3&7J!Zj2!DkR25`4QX~UfX;WOZ|ATKDbjycohTa??} zt|Jgo`H^xDzWJ~jQd-kQYZ!IawnNkh?fFZBgbRXeg##Z!$gjs*-#dSmai(MEQ0|zS zw=8C2a(yk0UBJA9SL3O(dG2^yDKqnwr8u)^^Onu*L|o%h_?oAQTk|x*#8WaBZh;UE z4vaCIA4Kp21S0@!JZ{r{g=H7QLLJTQgH~-%0)W^Z+4C$i{4_G`n$^gmTIA4;U`Z{} zwMca3=PvzA-{T8^9QgCVKQB}w=cgiPt(C3(^UYd89jqZRNY=mGvb(uLSI9yYa_37J?fkJS1{AmDWWRPteIVl}kAyuTV6ES+7A?spfe z(RV5*&Q=G`mf%@4dT)>2xW0V7q7MqiwrLTKOla|M(7`gq1-!XIDJHk4x&@`px0>GC zy0_TjtEk(>ZLQu+W#PZDd!xiXPaM3sdV90izQGFA!p<|yiD9iNGO3?>h3_W(WP+s* z_~Ltj_z?h}0@v?n0`w#x2{+u(tpcj@R7}e@noWx)x1i$iQz@ht`h34^n;eB5_5}c- z({S&t+|6A1L-*ZkI9?0KOUkOzw_LdO`OVMEX4M#~8AB!Yd5=*#wbr-G{ou*5O5b?3 zZ@ktwUI~pumV9RHdur@+C#pugX2dIc+)ofc2JQ(HRoG~FOrcK9X#bZw)&D`A=+el2 zQ)>^R?S=v&75|sWh3)6t)RVD%XH7u(Yf*^}D!~N4NgEuc<);8_QU>hVtcjZ!cbyB0>(hKX_o-$NdfbK(~1n9AkM85^}>)IjY0qDLh(Bo?Y zb%K6HJAw-V;X*)94hr;lN1#p+l(k)0y>$X#LX<1s?Q5Y6<9jx9$x~vr58?;#nm 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 0000000000000000000000000000000000000000..60d984d0ea5645c2e8a9660a3adfdc83869a62fb GIT binary patch literal 253 zcmZ3^%ge<81dA(Jvy6fCV-N=hn4pZ$5a44*;f{&La} zElw>e)-TB@N=;46F9kAPQp-|v@(WUn^ixt(3yM=yvvm^-GW828OEU8F^ozm9>&M4u z=4F<|$LkeT{^GF7%}*)KNwq8D1R4o)N3jx+_`uA_$asT6<^n2uz%AUN)xZvdMeIOj E0Bl!6XaE2J literal 0 HcmV?d00001 diff --git a/solver/__pycache__/instance.cpython-311.pyc b/solver/__pycache__/instance.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f403f59a54cef8603913c95d5104e04f2427b05d GIT binary patch literal 9635 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 literal 0 HcmV?d00001 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('