deepseek-local-api 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -0
- package/README.md +138 -0
- package/bin/cli.js +66 -0
- package/package.json +49 -0
- package/src/client/ChatSession.js +187 -0
- package/src/client/DeepseekClient.js +134 -0
- package/src/config/constants.js +29 -0
- package/src/config/headers.js +45 -0
- package/src/index.js +307 -0
- package/src/services/PowService.js +66 -0
- package/src/services/WasmService.js +117 -0
- package/src/services/autocomplete.js +48 -0
- package/src/services/server.js +289 -0
- package/src/utils/encoding.js +94 -0
- package/src/utils/memory.js +65 -0
- package/wasm/sha3_wasm_bg.7b9ca65ddd.wasm +0 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const DeepseekClient = require('../client/DeepseekClient');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Retrieve the local network IPv4 address for display when network access is enabled
|
|
7
|
+
*/
|
|
8
|
+
function getLocalNetworkIp() {
|
|
9
|
+
const interfaces = os.networkInterfaces();
|
|
10
|
+
for (const name of Object.keys(interfaces)) {
|
|
11
|
+
for (const iface of interfaces[name]) {
|
|
12
|
+
if (iface.family === 'IPv4' && !iface.internal) {
|
|
13
|
+
return iface.address;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return '127.0.0.1';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Safely parse JSON request body with size limitation
|
|
22
|
+
*/
|
|
23
|
+
function parseJsonBody(req) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
let body = '';
|
|
26
|
+
req.on('data', (chunk) => {
|
|
27
|
+
body += chunk;
|
|
28
|
+
if (body.length > 10 * 1024 * 1024) { // 10MB limit
|
|
29
|
+
req.destroy();
|
|
30
|
+
reject(new Error('Payload too large'));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
req.on('end', () => {
|
|
34
|
+
if (!body.trim()) return resolve({});
|
|
35
|
+
try {
|
|
36
|
+
resolve(JSON.parse(body));
|
|
37
|
+
} catch (e) {
|
|
38
|
+
reject(new Error('Invalid JSON: ' + e.message));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
req.on('error', reject);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Attach CORS headers to response
|
|
47
|
+
*/
|
|
48
|
+
function setCorsHeaders(res) {
|
|
49
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
50
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
51
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Convert OpenAI messages array into a prompt for DeepSeek
|
|
56
|
+
*/
|
|
57
|
+
function formatMessagesToPrompt(messages) {
|
|
58
|
+
if (typeof messages === 'string') return messages;
|
|
59
|
+
if (!Array.isArray(messages) || messages.length === 0) return '';
|
|
60
|
+
|
|
61
|
+
if (messages.length === 1) {
|
|
62
|
+
return messages[0].content || '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const parts = [];
|
|
66
|
+
for (const msg of messages) {
|
|
67
|
+
const role = msg.role || 'user';
|
|
68
|
+
const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content);
|
|
69
|
+
if (role === 'system') {
|
|
70
|
+
parts.push(`[System]: ${content}`);
|
|
71
|
+
} else if (role === 'user') {
|
|
72
|
+
parts.push(`User: ${content}`);
|
|
73
|
+
} else if (role === 'assistant') {
|
|
74
|
+
parts.push(`Assistant: ${content}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return parts.join('\n\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Start OpenAI-compatible HTTP server
|
|
82
|
+
* @param {Object} options
|
|
83
|
+
* @param {string} options.token - DeepSeek token
|
|
84
|
+
* @param {number} [options.port=3000] - Port number to listen on
|
|
85
|
+
* @param {boolean} [options.isNetworkAvailable=false] - True to bind 0.0.0.0 (LAN access), false for 127.0.0.1 (localhost only)
|
|
86
|
+
* @returns {Promise<http.Server>}
|
|
87
|
+
*/
|
|
88
|
+
async function startServer({ token, port = 3000, isNetworkAvailable = false }) {
|
|
89
|
+
const client = new DeepseekClient(token);
|
|
90
|
+
process.stdout.write('Initializing DeepSeek client & WASM solver for local server... ');
|
|
91
|
+
await client.initialize();
|
|
92
|
+
console.log('Done.\n');
|
|
93
|
+
|
|
94
|
+
const host = isNetworkAvailable ? '0.0.0.0' : '127.0.0.1';
|
|
95
|
+
|
|
96
|
+
const server = http.createServer(async (req, res) => {
|
|
97
|
+
setCorsHeaders(res);
|
|
98
|
+
|
|
99
|
+
// Handle CORS preflight
|
|
100
|
+
if (req.method === 'OPTIONS') {
|
|
101
|
+
res.writeHead(204);
|
|
102
|
+
res.end();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const parsedUrl = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
|
|
107
|
+
const pathname = parsedUrl.pathname.replace(/\/+$/, '') || '/';
|
|
108
|
+
|
|
109
|
+
// Health check endpoint
|
|
110
|
+
if (req.method === 'GET' && (pathname === '/' || pathname === '/health')) {
|
|
111
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
112
|
+
res.end(JSON.stringify({ status: 'ok', service: 'deepseek-local-api' }));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// OpenAI Models endpoint
|
|
117
|
+
if (req.method === 'GET' && (pathname === '/v1/models' || pathname === '/models')) {
|
|
118
|
+
const models = {
|
|
119
|
+
object: 'list',
|
|
120
|
+
data: [
|
|
121
|
+
{ id: 'deepseek-chat', object: 'model', created: 1700000000, owned_by: 'deepseek' },
|
|
122
|
+
{ id: 'deepseek-reasoner', object: 'model', created: 1700000000, owned_by: 'deepseek' }
|
|
123
|
+
]
|
|
124
|
+
};
|
|
125
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
126
|
+
res.end(JSON.stringify(models));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// OpenAI Chat Completions endpoint
|
|
131
|
+
if (req.method === 'POST' && (pathname === '/v1/chat/completions' || pathname === '/chat/completions')) {
|
|
132
|
+
try {
|
|
133
|
+
const body = await parseJsonBody(req);
|
|
134
|
+
const model = body.model || 'deepseek-chat';
|
|
135
|
+
const stream = Boolean(body.stream);
|
|
136
|
+
const isReasoner = model.includes('reasoner') || model.includes('r1');
|
|
137
|
+
const thinking_enabled = body.thinking_enabled !== undefined ? body.thinking_enabled : isReasoner;
|
|
138
|
+
const search_enabled = Boolean(body.search_enabled);
|
|
139
|
+
|
|
140
|
+
const prompt = formatMessagesToPrompt(body.messages || body.prompt);
|
|
141
|
+
if (!prompt) {
|
|
142
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
143
|
+
res.end(JSON.stringify({ error: { message: 'Missing messages or prompt in request body', type: 'invalid_request_error' } }));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Create a fresh session for this completion request
|
|
148
|
+
const session = await client.createSession();
|
|
149
|
+
const completionResponse = await client.sendMessage(prompt, session, {
|
|
150
|
+
thinking_enabled,
|
|
151
|
+
search_enabled
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const completionId = 'chatcmpl-' + Math.random().toString(36).substring(2, 15);
|
|
155
|
+
const createdTime = Math.floor(Date.now() / 1000);
|
|
156
|
+
|
|
157
|
+
if (stream) {
|
|
158
|
+
res.writeHead(200, {
|
|
159
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
160
|
+
'Cache-Control': 'no-cache, no-transform',
|
|
161
|
+
'Connection': 'keep-alive',
|
|
162
|
+
'X-Accel-Buffering': 'no'
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
for await (const chunk of client.streamResponse(completionResponse, session)) {
|
|
166
|
+
const delta = {};
|
|
167
|
+
if (chunk.type === 'thinking') {
|
|
168
|
+
delta.reasoning_content = chunk.text;
|
|
169
|
+
} else if (chunk.type === 'content') {
|
|
170
|
+
delta.content = chunk.text;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const chunkPayload = {
|
|
174
|
+
id: completionId,
|
|
175
|
+
object: 'chat.completion.chunk',
|
|
176
|
+
created: createdTime,
|
|
177
|
+
model: model,
|
|
178
|
+
choices: [
|
|
179
|
+
{
|
|
180
|
+
index: 0,
|
|
181
|
+
delta: delta,
|
|
182
|
+
finish_reason: null
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
};
|
|
186
|
+
res.write(`data: ${JSON.stringify(chunkPayload)}\n\n`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Final finish_reason chunk
|
|
190
|
+
const stopPayload = {
|
|
191
|
+
id: completionId,
|
|
192
|
+
object: 'chat.completion.chunk',
|
|
193
|
+
created: createdTime,
|
|
194
|
+
model: model,
|
|
195
|
+
choices: [
|
|
196
|
+
{
|
|
197
|
+
index: 0,
|
|
198
|
+
delta: {},
|
|
199
|
+
finish_reason: 'stop'
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
};
|
|
203
|
+
res.write(`data: ${JSON.stringify(stopPayload)}\n\n`);
|
|
204
|
+
res.write('data: [DONE]\n\n');
|
|
205
|
+
res.end();
|
|
206
|
+
} else {
|
|
207
|
+
// Non-streaming response
|
|
208
|
+
let fullContent = '';
|
|
209
|
+
let fullThinking = '';
|
|
210
|
+
|
|
211
|
+
for await (const chunk of client.streamResponse(completionResponse, session)) {
|
|
212
|
+
if (chunk.type === 'thinking') {
|
|
213
|
+
fullThinking += chunk.text;
|
|
214
|
+
} else if (chunk.type === 'content') {
|
|
215
|
+
fullContent += chunk.text;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const result = {
|
|
220
|
+
id: completionId,
|
|
221
|
+
object: 'chat.completion',
|
|
222
|
+
created: createdTime,
|
|
223
|
+
model: model,
|
|
224
|
+
choices: [
|
|
225
|
+
{
|
|
226
|
+
index: 0,
|
|
227
|
+
message: {
|
|
228
|
+
role: 'assistant',
|
|
229
|
+
content: fullContent,
|
|
230
|
+
reasoning_content: fullThinking || undefined
|
|
231
|
+
},
|
|
232
|
+
finish_reason: 'stop'
|
|
233
|
+
}
|
|
234
|
+
],
|
|
235
|
+
usage: {
|
|
236
|
+
prompt_tokens: 0,
|
|
237
|
+
completion_tokens: 0,
|
|
238
|
+
total_tokens: 0
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
243
|
+
res.end(JSON.stringify(result));
|
|
244
|
+
}
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.error('Server error handling completion:', err);
|
|
247
|
+
if (!res.headersSent) {
|
|
248
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
249
|
+
res.end(JSON.stringify({ error: { message: err.message, type: 'server_error' } }));
|
|
250
|
+
} else {
|
|
251
|
+
res.end();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 404 Not Found
|
|
258
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
259
|
+
res.end(JSON.stringify({ error: { message: `Route not found: ${req.method} ${pathname}`, type: 'invalid_request_error' } }));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return new Promise((resolve, reject) => {
|
|
263
|
+
server.on('error', reject);
|
|
264
|
+
server.listen(port, host, () => {
|
|
265
|
+
const localIp = getLocalNetworkIp();
|
|
266
|
+
console.log('\n┌──────────────────────────────────────────────────────────────────────────');
|
|
267
|
+
console.log('│ 🚀 DeepSeek OpenAI-Compatible Local Server Running');
|
|
268
|
+
console.log('├──────────────────────────────────────────────────────────────────────────');
|
|
269
|
+
console.log(`│ 📍 Local Base URL : http://localhost:${port}/v1`);
|
|
270
|
+
if (isNetworkAvailable) {
|
|
271
|
+
console.log(`│ 🌐 Network Base URL : http://${localIp}:${port}/v1`);
|
|
272
|
+
console.log(`│ 🔓 Network Access : Enabled (0.0.0.0 - accessible to LAN devices)`);
|
|
273
|
+
} else {
|
|
274
|
+
console.log(`│ 🔒 Network Access : Disabled (127.0.0.1 - localhost only)`);
|
|
275
|
+
}
|
|
276
|
+
console.log('├──────────────────────────────────────────────────────────────────────────');
|
|
277
|
+
console.log(`│ 💬 Chat Endpoint : POST http://localhost:${port}/v1/chat/completions`);
|
|
278
|
+
console.log(`│ 📋 Models Endpoint : GET http://localhost:${port}/v1/models`);
|
|
279
|
+
console.log('└──────────────────────────────────────────────────────────────────────────\n');
|
|
280
|
+
resolve(server);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = {
|
|
286
|
+
startServer,
|
|
287
|
+
parseJsonBody,
|
|
288
|
+
formatMessagesToPrompt
|
|
289
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for handling string encoding and base64 operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
class EncodingUtils {
|
|
6
|
+
/**
|
|
7
|
+
* Encode a string to UTF-8 bytes
|
|
8
|
+
* @param {string} text - Text to encode
|
|
9
|
+
* @returns {Uint8Array} Encoded bytes
|
|
10
|
+
*/
|
|
11
|
+
static encodeUTF8(text) {
|
|
12
|
+
return new TextEncoder().encode(text);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Decode UTF-8 bytes to string
|
|
17
|
+
* @param {Uint8Array} bytes - Bytes to decode
|
|
18
|
+
* @returns {string} Decoded text
|
|
19
|
+
*/
|
|
20
|
+
static decodeUTF8(bytes) {
|
|
21
|
+
return new TextDecoder().decode(bytes);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Encode data to base64
|
|
26
|
+
* @param {string} data - String to encode
|
|
27
|
+
* @returns {string} Base64 encoded string
|
|
28
|
+
*/
|
|
29
|
+
static encodeBase64(data) {
|
|
30
|
+
if (typeof window !== 'undefined') {
|
|
31
|
+
// Browser environment
|
|
32
|
+
return btoa(data);
|
|
33
|
+
} else {
|
|
34
|
+
// Node.js environment
|
|
35
|
+
return Buffer.from(data).toString('base64');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Decode base64 string
|
|
41
|
+
* @param {string} base64 - Base64 string to decode
|
|
42
|
+
* @returns {string} Decoded string
|
|
43
|
+
*/
|
|
44
|
+
static decodeBase64(base64) {
|
|
45
|
+
if (typeof window !== 'undefined') {
|
|
46
|
+
// Browser environment
|
|
47
|
+
return atob(base64);
|
|
48
|
+
} else {
|
|
49
|
+
// Node.js environment
|
|
50
|
+
return Buffer.from(base64, 'base64').toString();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Encode JSON object to base64
|
|
56
|
+
* @param {Object} data - Object to encode
|
|
57
|
+
* @returns {string} Base64 encoded string
|
|
58
|
+
*/
|
|
59
|
+
static encodeJSONToBase64(data) {
|
|
60
|
+
const jsonStr = JSON.stringify(data);
|
|
61
|
+
return this.encodeBase64(jsonStr);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Decode base64 string to JSON object
|
|
66
|
+
* @param {string} base64 - Base64 string to decode
|
|
67
|
+
* @returns {Object} Decoded JSON object
|
|
68
|
+
*/
|
|
69
|
+
static decodeBase64ToJSON(base64) {
|
|
70
|
+
const jsonStr = this.decodeBase64(base64);
|
|
71
|
+
return JSON.parse(jsonStr);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Convert string to bytes with specified encoding
|
|
76
|
+
* @param {string} text - Text to convert
|
|
77
|
+
* @param {string} encoding - Encoding to use (utf8, base64, etc.)
|
|
78
|
+
* @returns {Uint8Array} Converted bytes
|
|
79
|
+
*/
|
|
80
|
+
static stringToBytes(text, encoding = 'utf8') {
|
|
81
|
+
switch (encoding.toLowerCase()) {
|
|
82
|
+
case 'utf8':
|
|
83
|
+
case 'utf-8':
|
|
84
|
+
return this.encodeUTF8(text);
|
|
85
|
+
case 'base64':
|
|
86
|
+
const decoded = this.decodeBase64(text);
|
|
87
|
+
return this.encodeUTF8(decoded);
|
|
88
|
+
default:
|
|
89
|
+
throw new Error(`Unsupported encoding: ${encoding}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = {EncodingUtils};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for handling WebAssembly memory operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
class MemoryUtils {
|
|
6
|
+
/**
|
|
7
|
+
* Write data to WebAssembly memory
|
|
8
|
+
* @param {DataView} view - DataView of the WebAssembly memory
|
|
9
|
+
* @param {number} offset - Memory offset to write to
|
|
10
|
+
* @param {Uint8Array} data - Data to write
|
|
11
|
+
*/
|
|
12
|
+
static writeToMemory(view, offset, data) {
|
|
13
|
+
for (let i = 0; i < data.length; i++) {
|
|
14
|
+
view.setUint8(offset + i, data[i]);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read data from WebAssembly memory
|
|
20
|
+
* @param {DataView} view - DataView of the WebAssembly memory
|
|
21
|
+
* @param {number} offset - Memory offset to read from
|
|
22
|
+
* @param {number} size - Number of bytes to read
|
|
23
|
+
* @returns {Uint8Array} Read data
|
|
24
|
+
*/
|
|
25
|
+
static readFromMemory(view, offset, size) {
|
|
26
|
+
const bytes = new Uint8Array(size);
|
|
27
|
+
for (let i = 0; i < size; i++) {
|
|
28
|
+
bytes[i] = view.getUint8(offset + i);
|
|
29
|
+
}
|
|
30
|
+
return bytes;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Read a 32-bit integer from memory
|
|
35
|
+
* @param {DataView} view - DataView of the WebAssembly memory
|
|
36
|
+
* @param {number} offset - Memory offset to read from
|
|
37
|
+
* @param {boolean} littleEndian - Whether to use little-endian byte order
|
|
38
|
+
* @returns {number} Read integer
|
|
39
|
+
*/
|
|
40
|
+
static readInt32(view, offset, littleEndian = true) {
|
|
41
|
+
return view.getInt32(offset, littleEndian);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read a 64-bit float from memory
|
|
46
|
+
* @param {DataView} view - DataView of the WebAssembly memory
|
|
47
|
+
* @param {number} offset - Memory offset to read from
|
|
48
|
+
* @param {boolean} littleEndian - Whether to use little-endian byte order
|
|
49
|
+
* @returns {number} Read float
|
|
50
|
+
*/
|
|
51
|
+
static readFloat64(view, offset, littleEndian = true) {
|
|
52
|
+
return view.getFloat64(offset, littleEndian);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Create a DataView from a WebAssembly memory buffer
|
|
57
|
+
* @param {WebAssembly.Memory} memory - WebAssembly memory instance
|
|
58
|
+
* @returns {DataView} DataView of the memory
|
|
59
|
+
*/
|
|
60
|
+
static createMemoryView(memory) {
|
|
61
|
+
return new DataView(memory.buffer);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {MemoryUtils};
|
|
Binary file
|