initial commit
This commit is contained in:
@@ -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
|
||||
+14
@@ -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"]
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Python DeepSeek R1 API Implementation
|
||||
@@ -0,0 +1,3 @@
|
||||
from .client import Client
|
||||
|
||||
__all__ = ['Client']
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+358
@@ -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<thinking>\n")
|
||||
search = True
|
||||
if p == "response/thinking_content":
|
||||
if not search:
|
||||
response_callback("\n<thinking>\n")
|
||||
response_callback("\n\n")
|
||||
think = True
|
||||
elif p == "response/content":
|
||||
if think:
|
||||
response_callback("\n</thinking>\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
|
||||
+149
@@ -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]
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
API utilities - empty utilities for compatibility
|
||||
"""
|
||||
@@ -0,0 +1,3 @@
|
||||
from .app import Application
|
||||
|
||||
__all__ = ['Application']
|
||||
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
@@ -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))
|
||||
@@ -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:
|
||||
@@ -0,0 +1,12 @@
|
||||
from .models import ChatCompletionRequest, Message, ChatCompletionResponse, Choice, ChunkResponse, ChunkChoice, Delta, Model
|
||||
|
||||
__all__ = [
|
||||
'ChatCompletionRequest',
|
||||
'Message',
|
||||
'ChatCompletionResponse',
|
||||
'Choice',
|
||||
'ChunkResponse',
|
||||
'ChunkChoice',
|
||||
'Delta',
|
||||
'Model',
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
+137
@@ -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,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Utilities module - empty utilities for compatibility
|
||||
"""
|
||||
@@ -0,0 +1,3 @@
|
||||
from .cache import Cache, ChatData
|
||||
|
||||
__all__ = ['Cache', 'ChatData']
|
||||
Binary file not shown.
Binary file not shown.
+64
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
from .instance import Solver
|
||||
|
||||
__all__ = ['Solver']
|
||||
Binary file not shown.
Binary file not shown.
@@ -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('<I', status_bytes)[0]
|
||||
print(f"[DEBUG] Status: {status}")
|
||||
|
||||
if status == 0:
|
||||
raise Exception("No solution found")
|
||||
|
||||
# Read the answer as float64
|
||||
value_bytes = self._read_memory(retptr + 8, 8)
|
||||
value = struct.unpack('<d', value_bytes)[0]
|
||||
print(f"[DEBUG] Value from memory: {value}")
|
||||
|
||||
# Convert float to int
|
||||
answer = int(value)
|
||||
print(f"[DEBUG] Final answer: {answer}")
|
||||
|
||||
# Reset stack pointer - pass store as first argument
|
||||
self.stack_ptr_fn(self.store, 16)
|
||||
|
||||
return answer
|
||||
|
||||
def close(self):
|
||||
"""Close the solver"""
|
||||
self.store = None
|
||||
self.instance = None
|
||||
Reference in New Issue
Block a user