initial commit

This commit is contained in:
2026-05-14 18:38:09 +03:00
commit f41d64f39b
32 changed files with 1469 additions and 0 deletions
+5
View File
@@ -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
View File
@@ -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"]
+224
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
# Python DeepSeek R1 API Implementation
+3
View File
@@ -0,0 +1,3 @@
from .client import Client
__all__ = ['Client']
Binary file not shown.
Binary file not shown.
Binary file not shown.
+358
View File
@@ -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
View File
@@ -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]
+3
View File
@@ -0,0 +1,3 @@
"""
API utilities - empty utilities for compatibility
"""
+3
View File
@@ -0,0 +1,3 @@
from .app import Application
__all__ = ['Application']
Binary file not shown.
Binary file not shown.
+219
View File
@@ -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()
+6
View File
@@ -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))
+24
View File
@@ -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:
+12
View File
@@ -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
View File
@@ -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,
}
+3
View File
@@ -0,0 +1,3 @@
"""
Utilities module - empty utilities for compatibility
"""
+3
View File
@@ -0,0 +1,3 @@
from .cache import Cache, ChatData
__all__ = ['Cache', 'ChatData']
Binary file not shown.
Binary file not shown.
+64
View File
@@ -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()
+68
View File
@@ -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()
+5
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
from .instance import Solver
__all__ = ['Solver']
Binary file not shown.
Binary file not shown.
+165
View File
@@ -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