Update server.js
Browse files
server.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
| 1 |
import express from 'express';
|
| 2 |
import cors from 'cors';
|
| 3 |
import rateLimit from 'express-rate-limit';
|
| 4 |
-
import {
|
| 5 |
-
import crypto from 'crypto';
|
| 6 |
|
| 7 |
const app = express();
|
| 8 |
const PORT = 7860;
|
|
@@ -25,174 +24,99 @@ app.use((req, res, next) => {
|
|
| 25 |
app.use(express.json({ limit: '50mb' }));
|
| 26 |
app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
| 27 |
|
| 28 |
-
//
|
| 29 |
-
function getRandomUserAgent() {
|
| 30 |
-
const agents = [
|
| 31 |
-
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
| 32 |
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15",
|
| 33 |
-
"Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0"
|
| 34 |
-
];
|
| 35 |
-
return agents[Math.floor(Math.random() * agents.length)];
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
// Modelos soportados por DuckDuckGo AI
|
| 39 |
-
const DDG_MODELS = {
|
| 40 |
-
"gpt-4o-mini": "gpt-4o-mini",
|
| 41 |
-
"claude-3-haiku": "claude-3-haiku-20240307",
|
| 42 |
-
"llama-3.1-70b": "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
|
| 43 |
-
"mixtral-8x7b": "mistralai/Mixtral-8x7B-Instruct-v0.1"
|
| 44 |
-
};
|
| 45 |
-
|
| 46 |
-
// --- RATE LIMITING ---
|
| 47 |
const limiter = rateLimit({
|
| 48 |
windowMs: 60 * 1000,
|
| 49 |
max: 50,
|
| 50 |
keyGenerator: (req) => req.ip,
|
| 51 |
-
message: { error: { message: "Too many requests
|
| 52 |
standardHeaders: false,
|
| 53 |
legacyHeaders: false,
|
| 54 |
});
|
| 55 |
|
| 56 |
app.get('/health', (req, res) => {
|
| 57 |
-
res.json({ status: "online", proxy: "DuckDuckGo
|
| 58 |
});
|
| 59 |
|
| 60 |
-
// Endpoint de modelos (Formato OpenAI)
|
| 61 |
app.get('/v1/models', (req, res) => {
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
| 69 |
});
|
| 70 |
|
| 71 |
app.post('/v1/chat/completions', limiter, async (req, res) => {
|
| 72 |
-
const startTime = Date.now();
|
| 73 |
-
const clientIp = req.ip || req.headers['x-forwarded-for'] || req.socket.remoteAddress;
|
| 74 |
-
|
| 75 |
-
// Configuración del modelo y mensajes
|
| 76 |
-
const requestedModel = req.body.model || "gpt-4o-mini";
|
| 77 |
-
const duckModel = DDG_MODELS[requestedModel] || "gpt-4o-mini";
|
| 78 |
-
const messages = req.body.messages || [];
|
| 79 |
-
const isStream = req.body.stream === true;
|
| 80 |
-
|
| 81 |
try {
|
| 82 |
-
|
| 83 |
-
const statusRes = await fetch("https://duckduckgo.com/duckchat/v1/status", {
|
| 84 |
-
headers: {
|
| 85 |
-
"x-vqd-accept": "1",
|
| 86 |
-
"User-Agent": getRandomUserAgent(),
|
| 87 |
-
"x-forwarded-for": clientIp
|
| 88 |
-
}
|
| 89 |
-
});
|
| 90 |
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
"x-forwarded-for": clientIp
|
| 105 |
-
},
|
| 106 |
-
body: JSON.stringify({
|
| 107 |
-
model: duckModel,
|
| 108 |
-
messages: messages
|
| 109 |
-
})
|
| 110 |
-
});
|
| 111 |
-
|
| 112 |
-
if (!chatRes.ok) {
|
| 113 |
-
const errorText = await chatRes.text();
|
| 114 |
-
return res.status(chatRes.status).json({ error: { message: `DuckDuckGo Error: ${errorText}`, code: chatRes.status }});
|
| 115 |
-
}
|
| 116 |
|
| 117 |
-
//
|
| 118 |
-
if (!
|
| 119 |
-
|
| 120 |
-
const reader = chatRes.body.getReader();
|
| 121 |
-
const decoder = new TextDecoder("utf-8");
|
| 122 |
-
|
| 123 |
-
while (true) {
|
| 124 |
-
const { done, value } = await reader.read();
|
| 125 |
-
if (done) break;
|
| 126 |
-
|
| 127 |
-
const chunkStr = decoder.decode(value, { stream: true });
|
| 128 |
-
const lines = chunkStr.split('\n');
|
| 129 |
-
for (let line of lines) {
|
| 130 |
-
if (line.startsWith('data: ') && line !== 'data: [DONE]') {
|
| 131 |
-
try {
|
| 132 |
-
const data = JSON.parse(line.slice(6));
|
| 133 |
-
if (data.message) fullText += data.message;
|
| 134 |
-
} catch (e) { /* Ignorar chunks rotos */ }
|
| 135 |
-
}
|
| 136 |
-
}
|
| 137 |
-
}
|
| 138 |
-
|
| 139 |
return res.json({
|
| 140 |
-
id: 'chatcmpl-' +
|
| 141 |
object: 'chat.completion',
|
| 142 |
created: Math.floor(Date.now() / 1000),
|
| 143 |
-
model:
|
| 144 |
-
choices: [{ message: { role: 'assistant', content:
|
| 145 |
});
|
| 146 |
}
|
| 147 |
|
| 148 |
-
//
|
| 149 |
res.writeHead(200, {
|
| 150 |
'Content-Type': 'text/event-stream',
|
| 151 |
'Cache-Control': 'no-cache',
|
| 152 |
'Connection': 'keep-alive'
|
| 153 |
});
|
| 154 |
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
try {
|
| 165 |
-
const data = JSON.parse(line.slice(6));
|
| 166 |
-
if (data.message) {
|
| 167 |
-
const openaiChunk = {
|
| 168 |
-
id: 'chatcmpl-' + crypto.randomUUID(),
|
| 169 |
-
object: 'chat.completion.chunk',
|
| 170 |
-
created: Math.floor(Date.now() / 1000),
|
| 171 |
-
model: requestedModel,
|
| 172 |
-
choices: [{ delta: { content: data.message } }]
|
| 173 |
-
};
|
| 174 |
-
this.push(`data: ${JSON.stringify(openaiChunk)}\n\n`);
|
| 175 |
-
}
|
| 176 |
-
} catch (e) { /* Ignorar errores de parseo parcial */ }
|
| 177 |
-
} else if (line === 'data: [DONE]') {
|
| 178 |
-
this.push('data: [DONE]\n\n');
|
| 179 |
-
}
|
| 180 |
-
}
|
| 181 |
-
callback();
|
| 182 |
-
}
|
| 183 |
});
|
| 184 |
|
| 185 |
-
|
| 186 |
-
|
| 187 |
|
| 188 |
} catch (err) {
|
| 189 |
-
console.error("[ERROR]", err
|
|
|
|
|
|
|
| 190 |
if (!res.headersSent) {
|
| 191 |
-
res.status(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
}
|
| 193 |
}
|
| 194 |
});
|
| 195 |
|
| 196 |
app.listen(PORT, '0.0.0.0', () => {
|
| 197 |
-
console.log(`[SYS] DuckDuckGo
|
| 198 |
});
|
|
|
|
| 1 |
import express from 'express';
|
| 2 |
import cors from 'cors';
|
| 3 |
import rateLimit from 'express-rate-limit';
|
| 4 |
+
import { DuckDuckGoChat, Models } from 'duckduckgo-chat-interface';
|
|
|
|
| 5 |
|
| 6 |
const app = express();
|
| 7 |
const PORT = 7860;
|
|
|
|
| 24 |
app.use(express.json({ limit: '50mb' }));
|
| 25 |
app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
| 26 |
|
| 27 |
+
// --- RATE LIMITING LOCAL ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
const limiter = rateLimit({
|
| 29 |
windowMs: 60 * 1000,
|
| 30 |
max: 50,
|
| 31 |
keyGenerator: (req) => req.ip,
|
| 32 |
+
message: { error: { message: "Too many requests.", code: 429 } },
|
| 33 |
standardHeaders: false,
|
| 34 |
legacyHeaders: false,
|
| 35 |
});
|
| 36 |
|
| 37 |
app.get('/health', (req, res) => {
|
| 38 |
+
res.json({ status: "online", proxy: "DuckDuckGo Library", node_module: "duckduckgo-chat-interface" });
|
| 39 |
});
|
| 40 |
|
|
|
|
| 41 |
app.get('/v1/models', (req, res) => {
|
| 42 |
+
res.json({
|
| 43 |
+
object: "list",
|
| 44 |
+
data: [
|
| 45 |
+
{ id: "gpt-4o-mini", object: "model", type: "text", owned_by: "duckduckgo" },
|
| 46 |
+
{ id: "claude-3-haiku", object: "model", type: "text", owned_by: "duckduckgo" },
|
| 47 |
+
{ id: "llama-3.1-70b", object: "model", type: "text", owned_by: "duckduckgo" },
|
| 48 |
+
{ id: "mixtral-8x7b", object: "model", type: "text", owned_by: "duckduckgo" }
|
| 49 |
+
]
|
| 50 |
+
});
|
| 51 |
});
|
| 52 |
|
| 53 |
app.post('/v1/chat/completions', limiter, async (req, res) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
try {
|
| 55 |
+
const { model = "gpt-4o-mini", messages, stream } = req.body;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
+
// 1. Mapear tu string de modelo al Enum que usa la librería
|
| 58 |
+
let chatModel = Models.GPT4Mini;
|
| 59 |
+
if (model.includes("claude")) chatModel = Models.Claude3Haiku;
|
| 60 |
+
if (model.includes("llama")) chatModel = Models.Llama3_3_70B;
|
| 61 |
+
if (model.includes("mistral") || model.includes("mixtral")) chatModel = Models.MistralSmall;
|
| 62 |
+
|
| 63 |
+
// 2. Inicializar la sesión de DuckDuckGo
|
| 64 |
+
const chat = new DuckDuckGoChat(chatModel);
|
| 65 |
+
await chat.initialize();
|
| 66 |
+
|
| 67 |
+
// Extraemos solo el mensaje del usuario (DuckDuckGo no soporta bien historiales complejos por API sin manejo avanzado)
|
| 68 |
+
// Concatenamos el historial para darle contexto al modelo si hay varios mensajes.
|
| 69 |
+
const fullPrompt = messages.map(m => `${m.role}: ${m.content}`).join('\n\n');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
+
// 3A. Modo Texto Completo (No Streaming)
|
| 72 |
+
if (!stream) {
|
| 73 |
+
const response = await chat.sendMessage(fullPrompt);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
return res.json({
|
| 75 |
+
id: 'chatcmpl-' + Date.now(),
|
| 76 |
object: 'chat.completion',
|
| 77 |
created: Math.floor(Date.now() / 1000),
|
| 78 |
+
model: model,
|
| 79 |
+
choices: [{ message: { role: 'assistant', content: response }, finish_reason: 'stop' }]
|
| 80 |
});
|
| 81 |
}
|
| 82 |
|
| 83 |
+
// 3B. Modo Streaming (Traducción en tiempo real a OpenAI)
|
| 84 |
res.writeHead(200, {
|
| 85 |
'Content-Type': 'text/event-stream',
|
| 86 |
'Cache-Control': 'no-cache',
|
| 87 |
'Connection': 'keep-alive'
|
| 88 |
});
|
| 89 |
|
| 90 |
+
await chat.sendMessageStream(fullPrompt, (chunk) => {
|
| 91 |
+
const data = {
|
| 92 |
+
id: 'chatcmpl-' + Date.now(),
|
| 93 |
+
object: 'chat.completion.chunk',
|
| 94 |
+
created: Math.floor(Date.now() / 1000),
|
| 95 |
+
model: model,
|
| 96 |
+
choices: [{ delta: { content: chunk } }]
|
| 97 |
+
};
|
| 98 |
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
});
|
| 100 |
|
| 101 |
+
res.write('data: [DONE]\n\n');
|
| 102 |
+
res.end();
|
| 103 |
|
| 104 |
} catch (err) {
|
| 105 |
+
console.error("[DDG LIBRARY ERROR]", err);
|
| 106 |
+
const statusCode = err.message.includes('503') ? 503 : 500;
|
| 107 |
+
|
| 108 |
if (!res.headersSent) {
|
| 109 |
+
res.status(statusCode).json({
|
| 110 |
+
error: {
|
| 111 |
+
message: "Error de proveedor. Si recibes 503 constantemente, Cloudflare está bloqueando la IP de Hugging Face.",
|
| 112 |
+
details: err.message,
|
| 113 |
+
code: statusCode
|
| 114 |
+
}
|
| 115 |
+
});
|
| 116 |
}
|
| 117 |
}
|
| 118 |
});
|
| 119 |
|
| 120 |
app.listen(PORT, '0.0.0.0', () => {
|
| 121 |
+
console.log(`[SYS] DuckDuckGo Library Node.js Server Ready. Puerto: ${PORT}`);
|
| 122 |
});
|