Juanoto2012 commited on
Commit
d9c6f10
verified
1 Parent(s): 9226776

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +145 -138
server.js CHANGED
@@ -6,18 +6,30 @@ import { Readable } from 'stream';
6
  const app = express();
7
  const PORT = 7860;
8
 
9
- // Configuraci贸n CR脥TICA para que express-rate-limit lea la IP real del usuario
10
- // cuando la app est谩 detr谩s de proxies inversos (ej. Vercel, Render, HF Spaces, Nginx)
11
  app.set('trust proxy', 1);
 
 
12
  app.use(cors());
13
 
 
 
 
 
 
 
 
 
 
 
14
  // Aumentar l铆mite a 50mb para soportar im谩genes en Base64 (Visi贸n / Multimodal)
15
  app.use(express.json({ limit: '50mb' }));
16
  app.use(express.urlencoded({ limit: '50mb', extended: true }));
17
 
18
- // --- FUNCI脫N DE LOGS ---
19
  function logError(providerId, reason) {
20
  const timestamp = new Date().toISOString();
 
21
  console.error(`[${timestamp}] [ERROR] Proveedor: ${providerId} | Motivo: ${reason}`);
22
  }
23
 
@@ -27,12 +39,6 @@ const PROVIDERS = [
27
  id: "llm7",
28
  url: "https://api.llm7.io/v1/chat/completions"
29
  },
30
- {
31
- id: "pollinations",
32
- url: "https://text.pollinations.ai/openai",
33
- modelsUrl: "https://text.pollinations.ai/models",
34
- imageUrl: "https://image.pollinations.ai/prompt"
35
- },
36
  {
37
  id: "airforce",
38
  url: "https://api.airforce/v1/chat/completions",
@@ -48,14 +54,14 @@ const PROVIDERS = [
48
  const MAX_PER_PROVIDER = 3;
49
  const QUEUE_TIMEOUT = 25000;
50
 
51
- let currentLoad = { "llm7": 0, "pollinations": 0, "airforce": 0, "ventarys-mirror": 0 };
52
 
53
  // --- RATE LIMITING (Basado en la IP real del usuario) ---
54
  const limiter = rateLimit({
55
  windowMs: 60 * 1000,
56
  max: 25,
57
- keyGenerator: (req) => req.ip, // Extrae la IP real verificada por 'trust proxy'
58
- message: { error: { message: "L铆mite alcanzado. Espera 1 minuto entre mensajes.", code: 429 } },
59
  standardHeaders: true,
60
  legacyHeaders: false,
61
  });
@@ -66,7 +72,6 @@ const AUDIO_KEYWORDS = ["suno", "udio", "music", "audio", "song", "voice", "tts"
66
 
67
  function isImageModel(model) {
68
  if (!model) return false;
69
- // Soporte directo para el flag de api.airforce
70
  if (model.type === 'image' || model.supports_images === true) return true;
71
 
72
  const id = (model.id || model.name || "").toLowerCase();
@@ -99,9 +104,17 @@ async function fetchAllModels() {
99
 
100
  if (modelsArray.length > 0) {
101
  return modelsArray
102
- // Filtrar modelos de audio/m煤sica
103
  .filter(model => !isAudioModel(model))
104
- // Procesar y formatear campos
 
 
 
 
 
 
 
 
105
  .map(model => ({
106
  ...model,
107
  id: model.id || model.name,
@@ -138,10 +151,9 @@ app.get('/v1/models', async (req, res) => {
138
  const allModels = await fetchAllModels();
139
 
140
  const textModels = allModels
141
- // Excluir im谩genes y asegurar que soporte chat (filtro estricto para api.airforce)
142
  .filter(m => !isImageModel(m) && m.supports_chat !== false)
143
  .map(m => {
144
- m.type = 'text'; // Forzar etiqueta
145
  return m;
146
  });
147
 
@@ -158,14 +170,12 @@ app.get('/v1/images/models', async (req, res) => {
158
  const allModels = await fetchAllModels();
159
 
160
  let imageModels = allModels
161
- // Incluir expl铆citamente modelos de imagen (o que supports_images sea true en airforce)
162
  .filter(m => isImageModel(m))
163
  .map(m => {
164
- m.type = 'image'; // Forzar etiqueta
165
  return m;
166
  });
167
 
168
- // Garantizar que la App siempre reconozca los modelos base
169
  const baseImages = [
170
  { id: "flux", object: "model", type: "image", owned_by: "system", tier: "standard" },
171
  { id: "dall-e-3", object: "model", type: "image", owned_by: "system", tier: "standard" }
@@ -187,14 +197,16 @@ app.get('/v1/images/models', async (req, res) => {
187
  // --- RUTA PRINCIPAL DE GENERACI脫N ---
188
  app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req, res) => {
189
  const isImage = req.path === '/v1/images/generations';
190
- const availableProviders = isImage ? PROVIDERS.filter(p => p.imageUrl) : PROVIDERS;
191
 
 
192
  const startTime = Date.now();
193
- let selectedProvider = null;
194
 
195
- // Sistema de cola inteligente
196
- while (Date.now() - startTime < QUEUE_TIMEOUT) {
 
197
  let shuffled = [...availableProviders].sort(() => Math.random() - 0.5);
 
198
  for (let provider of shuffled) {
199
  if (currentLoad[provider.id] < MAX_PER_PROVIDER) {
200
  selectedProvider = provider;
@@ -202,144 +214,139 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
202
  break;
203
  }
204
  }
205
- if (selectedProvider) break;
206
- await new Promise(r => setTimeout(r, 1500));
207
- }
208
 
209
- if (!selectedProvider) {
210
- logError("ProxyMain", "Saturaci贸n - Todas las APIs est谩n ocupadas");
211
- return res.status(503).json({ error: { message: "Todas las APIs est谩n ocupadas. Por favor, reintenta.", code: 503 } });
212
- }
213
-
214
- let isReleased = false;
215
- const releaseSlot = () => {
216
- if (!isReleased) {
217
- currentLoad[selectedProvider.id] = Math.max(0, currentLoad[selectedProvider.id] - 1);
218
- isReleased = true;
219
  }
220
- };
221
-
222
- try {
223
- let targetUrl = isImage ? selectedProvider.imageUrl : selectedProvider.url;
224
- let reqMethod = "POST";
225
- let reqBody = JSON.stringify(req.body);
226
- const fetchHeaders = { "Content-Type": "application/json" };
227
-
228
- if (selectedProvider.apiKey) fetchHeaders["Authorization"] = `Bearer ${selectedProvider.apiKey}`;
229
- if (selectedProvider.proxySecret) fetchHeaders["X-Proxy-Secret"] = selectedProvider.proxySecret;
230
 
231
- // Adaptador especializado para Pollinations Im谩genes (Formato nativo GET)
232
- if (isImage && selectedProvider.id === "pollinations") {
233
- const prompt = req.body.prompt || "A random image";
234
- const seed = Math.floor(Math.random() * 10000000);
235
-
236
- let width = 1024, height = 1024;
237
- if (req.body.size) {
238
- const parts = req.body.size.split('x');
239
- if (parts.length === 2) { width = parseInt(parts[0]); height = parseInt(parts[1]); }
240
  }
241
-
242
- const pollModel = req.body.model && req.body.model.includes('flux') ? 'flux' : 'dall-e-3';
243
-
244
- targetUrl = `${targetUrl}/${encodeURIComponent(prompt)}?seed=${seed}&width=${width}&height=${height}&model=${pollModel}&nologo=true`;
245
- reqMethod = "GET";
246
- reqBody = undefined;
247
- delete fetchHeaders["Content-Type"];
248
- }
249
 
250
- const response = await fetch(targetUrl, {
251
- method: reqMethod,
252
- headers: fetchHeaders,
253
- body: reqBody
254
- });
 
 
 
255
 
256
- if (!response.ok) {
257
- logError(selectedProvider.id, `Respuesta HTTP ${response.status} - ${response.statusText}`);
258
- }
 
 
259
 
260
- // --- MANEJO EXCLUSIVO DE IM脕GENES (Estandarizaci贸n a Base64 - Estilo Aqua AI) ---
261
- if (isImage) {
262
- const contentType = response.headers.get("content-type") || "";
 
 
 
263
 
264
- if (contentType.includes("application/json")) {
265
- const jsonResp = await response.json();
266
-
267
- // Extraer el array de datos o crearlo si la API (ej. airforce) responde con { url: "..." } directo
268
- let dataArray = jsonResp.data;
269
- if (!dataArray && jsonResp.url) {
270
- dataArray = [{ url: jsonResp.url }];
271
- }
272
-
273
- // Convertir URLs de im谩genes a base64_json (Formato esperado id茅ntico a Aqua AI)
274
- if (dataArray && Array.isArray(dataArray)) {
275
- for (let item of dataArray) {
276
- if (item.url && !item.b64_json) {
277
- try {
278
- const imgRes = await fetch(item.url);
279
- const arrayBuffer = await imgRes.arrayBuffer();
280
- item.b64_json = Buffer.from(arrayBuffer).toString('base64');
281
- delete item.url;
282
- } catch (e) {
283
- logError(selectedProvider.id, `Fallo convirtiendo URL a Base64: ${e.message}`);
 
 
 
 
 
 
 
 
 
284
  }
285
  }
 
 
 
 
 
 
286
  }
 
287
  releaseSlot();
288
- return res.status(response.status).json({
 
 
 
 
 
 
 
 
 
289
  created: Math.floor(Date.now() / 1000),
290
- data: dataArray
291
  });
 
 
 
 
 
 
292
  }
293
-
294
- // Respaldo de seguridad si no encontr贸 formato extra铆ble
295
- releaseSlot();
296
- return res.status(response.status).json(jsonResp);
297
  }
298
- else if (contentType.includes("image/")) {
299
- // Si la API nos devuelve el binario directo (Ej. Pollinations)
300
- const arrayBuffer = await response.arrayBuffer();
301
- const b64 = Buffer.from(arrayBuffer).toString('base64');
302
- releaseSlot();
 
 
 
 
 
 
303
 
304
- // Formato Aqua AI Strict:
305
- return res.status(200).json({
306
- created: Math.floor(Date.now() / 1000),
307
- data: [{ b64_json: b64 }]
308
  });
309
- }
310
- else {
311
- const textResp = await response.text();
312
  releaseSlot();
313
- return res.status(response.status).type(contentType).send(textResp);
314
  }
315
- }
316
-
317
- // --- MANEJO EXCLUSIVO DE TEXTO (Streaming) ---
318
- res.writeHead(response.status, {
319
- 'Content-Type': response.headers.get('content-type') || 'text/event-stream',
320
- 'Cache-Control': 'no-cache',
321
- 'Connection': 'keep-alive'
322
- });
323
-
324
- if (response.body) {
325
- const stream = Readable.fromWeb(response.body);
326
- stream.pipe(res);
327
 
328
- stream.on('end', releaseSlot);
329
- stream.on('error', (err) => {
330
- logError(selectedProvider.id, `Stream roto a mitad de respuesta: ${err.message}`);
331
- releaseSlot();
332
- });
333
- req.on('close', releaseSlot);
334
- } else {
335
  releaseSlot();
336
- res.end();
337
  }
 
338
 
339
- } catch (err) {
340
- releaseSlot();
341
- logError(selectedProvider ? selectedProvider.id : "Proxy", `Fallo o Timeout conectando al proveedor: ${err.message}`);
342
- res.status(500).json({ error: { message: "Error de conexi贸n interna."} });
343
  }
344
  });
345
 
 
6
  const app = express();
7
  const PORT = 7860;
8
 
9
+ // --- CONFIGURACI脫N DE SEGURIDAD Y PRIVACIDAD ---
 
10
  app.set('trust proxy', 1);
11
+ app.disable('x-powered-by'); // Oculta que estamos usando Express
12
+
13
  app.use(cors());
14
 
15
+ // Inyecci贸n de cabeceras de seguridad est谩ndar
16
+ app.use((req, res, next) => {
17
+ res.setHeader('X-Content-Type-Options', 'nosniff');
18
+ res.setHeader('X-Frame-Options', 'DENY');
19
+ res.setHeader('X-XSS-Protection', '1; mode=block');
20
+ res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
21
+ res.setHeader('Referrer-Policy', 'no-referrer'); // Protege la privacidad del origen
22
+ next();
23
+ });
24
+
25
  // Aumentar l铆mite a 50mb para soportar im谩genes en Base64 (Visi贸n / Multimodal)
26
  app.use(express.json({ limit: '50mb' }));
27
  app.use(express.urlencoded({ limit: '50mb', extended: true }));
28
 
29
+ // --- FUNCI脫N DE LOGS (Sanitizada) ---
30
  function logError(providerId, reason) {
31
  const timestamp = new Date().toISOString();
32
+ // No guardamos IPs ni prompts de usuarios, solo el estado de nuestros proveedores
33
  console.error(`[${timestamp}] [ERROR] Proveedor: ${providerId} | Motivo: ${reason}`);
34
  }
35
 
 
39
  id: "llm7",
40
  url: "https://api.llm7.io/v1/chat/completions"
41
  },
 
 
 
 
 
 
42
  {
43
  id: "airforce",
44
  url: "https://api.airforce/v1/chat/completions",
 
54
  const MAX_PER_PROVIDER = 3;
55
  const QUEUE_TIMEOUT = 25000;
56
 
57
+ let currentLoad = { "llm7": 0, "airforce": 0, "ventarys-mirror": 0 };
58
 
59
  // --- RATE LIMITING (Basado en la IP real del usuario) ---
60
  const limiter = rateLimit({
61
  windowMs: 60 * 1000,
62
  max: 25,
63
+ keyGenerator: (req) => req.ip,
64
+ message: { error: { message: "L铆mite de solicitudes alcanzado. Por favor, espera un momento.", code: 429 } },
65
  standardHeaders: true,
66
  legacyHeaders: false,
67
  });
 
72
 
73
  function isImageModel(model) {
74
  if (!model) return false;
 
75
  if (model.type === 'image' || model.supports_images === true) return true;
76
 
77
  const id = (model.id || model.name || "").toLowerCase();
 
104
 
105
  if (modelsArray.length > 0) {
106
  return modelsArray
107
+ // 1. Filtrar modelos de audio/m煤sica
108
  .filter(model => !isAudioModel(model))
109
+ // 2. CR脥TICO: Permitir estrictamente modelos con precio 0 (si la API reporta el precio)
110
+ .filter(model => {
111
+ if (model.pricepermilliontokens !== undefined && model.pricepermilliontokens !== null) {
112
+ return model.pricepermilliontokens === 0;
113
+ }
114
+ // Si el proveedor (ej. llm7) no env铆a el campo de precio, lo asumimos como v谩lido
115
+ return true;
116
+ })
117
+ // 3. Procesar y formatear campos
118
  .map(model => ({
119
  ...model,
120
  id: model.id || model.name,
 
151
  const allModels = await fetchAllModels();
152
 
153
  const textModels = allModels
 
154
  .filter(m => !isImageModel(m) && m.supports_chat !== false)
155
  .map(m => {
156
+ m.type = 'text';
157
  return m;
158
  });
159
 
 
170
  const allModels = await fetchAllModels();
171
 
172
  let imageModels = allModels
 
173
  .filter(m => isImageModel(m))
174
  .map(m => {
175
+ m.type = 'image';
176
  return m;
177
  });
178
 
 
179
  const baseImages = [
180
  { id: "flux", object: "model", type: "image", owned_by: "system", tier: "standard" },
181
  { id: "dall-e-3", object: "model", type: "image", owned_by: "system", tier: "standard" }
 
197
  // --- RUTA PRINCIPAL DE GENERACI脫N ---
198
  app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req, res) => {
199
  const isImage = req.path === '/v1/images/generations';
 
200
 
201
+ let availableProviders = isImage ? PROVIDERS.filter(p => p.imageUrl) : [...PROVIDERS];
202
  const startTime = Date.now();
203
+ let responseSent = false;
204
 
205
+ while (availableProviders.length > 0 && Date.now() - startTime < QUEUE_TIMEOUT) {
206
+ let selectedProvider = null;
207
+
208
  let shuffled = [...availableProviders].sort(() => Math.random() - 0.5);
209
+
210
  for (let provider of shuffled) {
211
  if (currentLoad[provider.id] < MAX_PER_PROVIDER) {
212
  selectedProvider = provider;
 
214
  break;
215
  }
216
  }
 
 
 
217
 
218
+ if (!selectedProvider) {
219
+ await new Promise(r => setTimeout(r, 1500));
220
+ continue;
 
 
 
 
 
 
 
221
  }
 
 
 
 
 
 
 
 
 
 
222
 
223
+ let isReleased = false;
224
+ const releaseSlot = () => {
225
+ if (!isReleased) {
226
+ currentLoad[selectedProvider.id] = Math.max(0, currentLoad[selectedProvider.id] - 1);
227
+ isReleased = true;
 
 
 
 
228
  }
229
+ };
 
 
 
 
 
 
 
230
 
231
+ try {
232
+ let targetUrl = isImage ? selectedProvider.imageUrl : selectedProvider.url;
233
+ let reqMethod = "POST";
234
+ let reqBody = JSON.stringify(req.body);
235
+ const fetchHeaders = { "Content-Type": "application/json" };
236
+
237
+ if (selectedProvider.apiKey) fetchHeaders["Authorization"] = `Bearer ${selectedProvider.apiKey}`;
238
+ if (selectedProvider.proxySecret) fetchHeaders["X-Proxy-Secret"] = selectedProvider.proxySecret;
239
 
240
+ const response = await fetch(targetUrl, {
241
+ method: reqMethod,
242
+ headers: fetchHeaders,
243
+ body: reqBody
244
+ });
245
 
246
+ if (!response.ok) {
247
+ logError(selectedProvider.id, `Fallo con c贸digo HTTP ${response.status}`);
248
+ releaseSlot();
249
+ availableProviders = availableProviders.filter(p => p.id !== selectedProvider.id);
250
+ continue;
251
+ }
252
 
253
+ // Sanitizaci贸n: Evitar devolver cookies o cabeceras de servidor del proveedor original
254
+ const responseHeaders = new Headers(response.headers);
255
+ responseHeaders.delete('set-cookie');
256
+ responseHeaders.delete('server');
257
+ responseHeaders.delete('x-powered-by');
258
+ responseHeaders.delete('cf-ray');
259
+
260
+ if (isImage) {
261
+ const contentType = responseHeaders.get("content-type") || "";
262
+
263
+ if (contentType.includes("application/json")) {
264
+ const jsonResp = await response.json();
265
+
266
+ let dataArray = jsonResp.data;
267
+ if (!dataArray && jsonResp.url) {
268
+ dataArray = [{ url: jsonResp.url }];
269
+ }
270
+
271
+ if (dataArray && Array.isArray(dataArray)) {
272
+ for (let item of dataArray) {
273
+ if (item.url && !item.b64_json) {
274
+ try {
275
+ const imgRes = await fetch(item.url);
276
+ const arrayBuffer = await imgRes.arrayBuffer();
277
+ item.b64_json = Buffer.from(arrayBuffer).toString('base64');
278
+ delete item.url;
279
+ } catch (e) {
280
+ logError(selectedProvider.id, `Fallo convitiendo URL a Base64.`);
281
+ }
282
  }
283
  }
284
+ releaseSlot();
285
+ responseSent = true;
286
+ return res.status(response.status).json({
287
+ created: Math.floor(Date.now() / 1000),
288
+ data: dataArray
289
+ });
290
  }
291
+
292
  releaseSlot();
293
+ responseSent = true;
294
+ return res.status(response.status).json(jsonResp);
295
+ }
296
+ else if (contentType.includes("image/")) {
297
+ const arrayBuffer = await response.arrayBuffer();
298
+ const b64 = Buffer.from(arrayBuffer).toString('base64');
299
+ releaseSlot();
300
+
301
+ responseSent = true;
302
+ return res.status(200).json({
303
  created: Math.floor(Date.now() / 1000),
304
+ data: [{ b64_json: b64 }]
305
  });
306
+ }
307
+ else {
308
+ const textResp = await response.text();
309
+ releaseSlot();
310
+ responseSent = true;
311
+ return res.status(response.status).type(contentType).send(textResp);
312
  }
 
 
 
 
313
  }
314
+
315
+ // Streaming (Texto)
316
+ res.writeHead(response.status, {
317
+ 'Content-Type': responseHeaders.get('content-type') || 'text/event-stream',
318
+ 'Cache-Control': 'no-cache',
319
+ 'Connection': 'keep-alive'
320
+ });
321
+
322
+ if (response.body) {
323
+ const stream = Readable.fromWeb(response.body);
324
+ stream.pipe(res);
325
 
326
+ stream.on('end', releaseSlot);
327
+ stream.on('error', (err) => {
328
+ logError(selectedProvider.id, `Stream interrumpido.`);
329
+ releaseSlot();
330
  });
331
+ req.on('close', releaseSlot);
332
+ } else {
 
333
  releaseSlot();
334
+ res.end();
335
  }
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
+ responseSent = true;
338
+ return;
339
+
340
+ } catch (err) {
341
+ logError(selectedProvider.id, `Excepci贸n de red conectando al upstream.`);
 
 
342
  releaseSlot();
343
+ availableProviders = availableProviders.filter(p => p.id !== selectedProvider.id);
344
  }
345
+ }
346
 
347
+ if (!responseSent) {
348
+ logError("ProxyMain", "Todos los proveedores fallaron o se agot贸 el tiempo de espera.");
349
+ return res.status(503).json({ error: { message: "El servicio no est谩 disponible temporalmente. Int茅ntalo de nuevo.", code: 503 } });
 
350
  }
351
  });
352