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.
@@ -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};