Juanoto2012 commited on
Commit
b7b6c51
·
verified ·
1 Parent(s): 6e40657

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +49 -63
server.js CHANGED
@@ -7,71 +7,64 @@ 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
  app.use(cors());
13
 
14
- // Inyección de cabeceras de seguridad estándar
15
  app.use((req, res, next) => {
16
  res.setHeader('X-Content-Type-Options', 'nosniff');
17
  res.setHeader('X-Frame-Options', 'DENY');
18
  res.setHeader('X-XSS-Protection', '1; mode=block');
19
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
20
- res.setHeader('Referrer-Policy', 'no-referrer'); // Protege la privacidad del origen
21
  next();
22
  });
23
 
24
- // Aumentar límite a 50mb para soportar imágenes en Base64 (Visión / Multimodal)
25
  app.use(express.json({ limit: '50mb' }));
26
  app.use(express.urlencoded({ limit: '50mb', extended: true }));
27
 
28
- // --- FUNCIÓN DE LOGS (Sanitizada) ---
29
  function logError(providerId, reason) {
30
  const timestamp = new Date().toISOString();
31
- // No guardamos IPs ni prompts de usuarios, solo el estado de nuestros proveedores
32
  console.error(`[${timestamp}] [ERROR] Proveedor: ${providerId} | Motivo: ${reason}`);
33
  }
34
 
35
- // --- CONFIGURACIÓN DE PROVEEDORES ---
 
36
  const PROVIDERS = [
37
  {
38
- id: "oai",
39
- url: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/chat/completions"
 
40
  },
41
  {
42
- id: "ventarys-mirror",
43
- url: "https://arliai.com/v1/chat/completions",
44
- proxySecret: "sk-52650650a50f0v10vg150vs0v"
45
- },
46
- {
47
- id: "ventarys-mirror-2",
48
- url: "https://Juanoto2012-mirror-2.hf.space/v1/chat/completions",
49
- proxySecret: "sk-52650650a50f0v10vg150vs0v"
50
  }
51
  ];
52
 
53
- const MAX_PER_PROVIDER = 3;
 
 
54
  const QUEUE_TIMEOUT = 25000;
55
- let currentLoad = { "oai": 0, "ventarys-mirror": 0 , "ventarys-mirror-2": 0 };
56
 
57
- // --- RATE LIMITING (Basado en la IP real del usuario) ---
58
  const limiter = rateLimit({
59
  windowMs: 60 * 1000,
60
- max: 25,
61
  keyGenerator: (req) => req.ip,
62
- message: { error: { message: "Límite de solicitudes alcanzado. Por favor, espera un momento.", code: 429 } },
63
  standardHeaders: true,
64
  legacyHeaders: false,
65
  });
66
 
67
- // --- AYUDANTES PARA FILTRAR MODELOS ---
68
  const IMAGE_KEYWORDS = ["flux", "dall", "midjourney", "sdxl", "stable-diffusion", "image", "vision"];
69
- const AUDIO_KEYWORDS = ["suno", "udio", "music", "audio", "song", "voice", "tts", "whisper",];
70
 
71
  function isImageModel(model) {
72
  if (!model) return false;
73
  if (model.type === 'image' || model.supports_images === true) return true;
74
-
75
  const id = (model.id || model.name || "").toLowerCase();
76
  return IMAGE_KEYWORDS.some(kw => id.includes(kw));
77
  }
@@ -87,9 +80,6 @@ async function fetchAllModels() {
87
  const modelsUrl = provider.modelsUrl || provider.url.replace("/chat/completions", "/models");
88
  const fetchHeaders = { "Content-Type": "application/json" };
89
 
90
- if (provider.apiKey) fetchHeaders["Authorization"] = `Bearer ${provider.apiKey}`;
91
- if (provider.proxySecret) fetchHeaders["X-Proxy-Secret"] = provider.proxySecret;
92
-
93
  try {
94
  const resp = await fetch(modelsUrl, { method: "GET", headers: fetchHeaders });
95
  if (!resp.ok) return [];
@@ -102,17 +92,7 @@ async function fetchAllModels() {
102
 
103
  if (modelsArray.length > 0) {
104
  return modelsArray
105
- // 1. Filtrar modelos de audio/música
106
  .filter(model => !isAudioModel(model))
107
- // 2. CRÍTICO: Permitir estrictamente modelos con precio 0 (si la API reporta el precio)
108
- .filter(model => {
109
- if (model.pricepermilliontokens !== undefined && model.pricepermilliontokens !== null) {
110
- return model.pricepermilliontokens === 0;
111
- }
112
- // Si el proveedor (ej. llm7) no envía el campo de precio, lo asumimos como válido
113
- return true;
114
- })
115
- // 3. Procesar y formatear campos
116
  .map(model => ({
117
  ...model,
118
  id: model.id || model.name,
@@ -130,10 +110,18 @@ async function fetchAllModels() {
130
  results.forEach(result => {
131
  if (result.status === "fulfilled") allModels = allModels.concat(result.value);
132
  });
 
 
 
 
 
 
 
 
 
133
  return allModels;
134
  }
135
 
136
- // --- RUTAS INFORMATIVAS ---
137
  app.get('/health', (req, res) => {
138
  res.json({
139
  status: "online",
@@ -143,11 +131,9 @@ app.get('/health', (req, res) => {
143
  });
144
  });
145
 
146
- // Endpoint para modelos de TEXTO
147
  app.get('/v1/models', async (req, res) => {
148
  try {
149
  const allModels = await fetchAllModels();
150
-
151
  const textModels = allModels
152
  .filter(m => !isImageModel(m) && m.supports_chat !== false)
153
  .map(m => {
@@ -162,11 +148,9 @@ app.get('/v1/models', async (req, res) => {
162
  }
163
  });
164
 
165
- // Endpoint para modelos de IMAGEN
166
  app.get('/v1/images/models', async (req, res) => {
167
  try {
168
  const allModels = await fetchAllModels();
169
-
170
  let imageModels = allModels
171
  .filter(m => isImageModel(m))
172
  .map(m => {
@@ -192,17 +176,21 @@ app.get('/v1/images/models', async (req, res) => {
192
  }
193
  });
194
 
195
- // --- RUTA PRINCIPAL DE GENERACIÓN ---
196
  app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req, res) => {
197
  const isImage = req.path === '/v1/images/generations';
198
-
199
  let availableProviders = isImage ? PROVIDERS.filter(p => p.imageUrl) : [...PROVIDERS];
 
 
 
 
200
  const startTime = Date.now();
201
  let responseSent = false;
202
 
 
 
 
203
  while (availableProviders.length > 0 && Date.now() - startTime < QUEUE_TIMEOUT) {
204
  let selectedProvider = null;
205
-
206
  let shuffled = [...availableProviders].sort(() => Math.random() - 0.5);
207
 
208
  for (let provider of shuffled) {
@@ -227,28 +215,29 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
227
  };
228
 
229
  try {
230
- let targetUrl = isImage ? selectedProvider.imageUrl : selectedProvider.url;
231
- let reqMethod = "POST";
232
- let reqBody = JSON.stringify(req.body);
233
- const fetchHeaders = { "Content-Type": "application/json" };
234
 
235
- if (selectedProvider.apiKey) fetchHeaders["Authorization"] = `Bearer ${selectedProvider.apiKey}`;
236
- if (selectedProvider.proxySecret) fetchHeaders["X-Proxy-Secret"] = selectedProvider.proxySecret;
 
 
 
 
 
237
 
238
  const response = await fetch(targetUrl, {
239
- method: reqMethod,
240
  headers: fetchHeaders,
241
- body: reqBody
242
  });
243
 
244
  if (!response.ok) {
245
- logError(selectedProvider.id, `Fallo con código HTTP ${response.status}`);
246
  releaseSlot();
247
  availableProviders = availableProviders.filter(p => p.id !== selectedProvider.id);
248
  continue;
249
  }
250
 
251
- // Sanitización: Evitar devolver cookies o cabeceras de servidor del proveedor original
252
  const responseHeaders = new Headers(response.headers);
253
  responseHeaders.delete('set-cookie');
254
  responseHeaders.delete('server');
@@ -260,11 +249,8 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
260
 
261
  if (contentType.includes("application/json")) {
262
  const jsonResp = await response.json();
263
-
264
  let dataArray = jsonResp.data;
265
- if (!dataArray && jsonResp.url) {
266
- dataArray = [{ url: jsonResp.url }];
267
- }
268
 
269
  if (dataArray && Array.isArray(dataArray)) {
270
  for (let item of dataArray) {
@@ -343,11 +329,11 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
343
  }
344
 
345
  if (!responseSent) {
346
- logError("ProxyMain", "Todos los proveedores fallaron o se agotó el tiempo de espera.");
347
- return res.status(503).json({ error: { message: "El servicio no está disponible temporalmente. Inténtalo de nuevo.", code: 503 } });
348
  }
349
  });
350
 
351
  app.listen(PORT, '0.0.0.0', () => {
352
- console.log(`🚀 API Proxy (Node.js) corriendo seguro en el puerto ${PORT}`);
353
  });
 
7
  const PORT = 7860;
8
 
9
  // --- CONFIGURACIÓN DE SEGURIDAD Y PRIVACIDAD ---
10
+ // CRÍTICO: Confiar en el proxy es necesario para que req.ip lea la IP real del usuario y no la de Cloudflare/Host
11
  app.set('trust proxy', 1);
12
+ app.disable('x-powered-by');
13
  app.use(cors());
14
 
 
15
  app.use((req, res, next) => {
16
  res.setHeader('X-Content-Type-Options', 'nosniff');
17
  res.setHeader('X-Frame-Options', 'DENY');
18
  res.setHeader('X-XSS-Protection', '1; mode=block');
19
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
20
+ res.setHeader('Referrer-Policy', 'no-referrer');
21
  next();
22
  });
23
 
 
24
  app.use(express.json({ limit: '50mb' }));
25
  app.use(express.urlencoded({ limit: '50mb', extended: true }));
26
 
 
27
  function logError(providerId, reason) {
28
  const timestamp = new Date().toISOString();
 
29
  console.error(`[${timestamp}] [ERROR] Proveedor: ${providerId} | Motivo: ${reason}`);
30
  }
31
 
32
+ // --- NUEVOS PROVEEDORES PÚBLICOS (SIN API KEY) ---
33
+ // Se eliminaron los HF Spaces. Se añaden endpoints gratuitos compatibles con OpenAI.
34
  const PROVIDERS = [
35
  {
36
+ id: "pollinations-ai",
37
+ url: "https://text.pollinations.ai/openai/v1/chat/completions",
38
+ modelsUrl: "https://text.pollinations.ai/openai/models"
39
  },
40
  {
41
+ id: "kepler-cloud",
42
+ url: "https://oai.endpoints.kepler.ai.cloud.ovh.net/v1/chat/completions"
 
 
 
 
 
 
43
  }
44
  ];
45
 
46
+ // Hemos subido el límite drásticamente. Ahora el proxy no será el cuello de botella.
47
+ // El límite real lo dictarán los proveedores según la IP que les reenviemos.
48
+ const MAX_PER_PROVIDER = 100;
49
  const QUEUE_TIMEOUT = 25000;
50
+ let currentLoad = { "pollinations-ai": 0, "kepler-cloud": 0 };
51
 
52
+ // --- RATE LIMITING LOCAL (Protección de tu propio servidor) ---
53
  const limiter = rateLimit({
54
  windowMs: 60 * 1000,
55
+ max: 50, // Aumentado a 50 peticiones por minuto por IP
56
  keyGenerator: (req) => req.ip,
57
+ message: { error: { message: "Estás enviando demasiadas peticiones. Espera un momento.", code: 429 } },
58
  standardHeaders: true,
59
  legacyHeaders: false,
60
  });
61
 
 
62
  const IMAGE_KEYWORDS = ["flux", "dall", "midjourney", "sdxl", "stable-diffusion", "image", "vision"];
63
+ const AUDIO_KEYWORDS = ["suno", "udio", "music", "audio", "song", "voice", "tts", "whisper"];
64
 
65
  function isImageModel(model) {
66
  if (!model) return false;
67
  if (model.type === 'image' || model.supports_images === true) return true;
 
68
  const id = (model.id || model.name || "").toLowerCase();
69
  return IMAGE_KEYWORDS.some(kw => id.includes(kw));
70
  }
 
80
  const modelsUrl = provider.modelsUrl || provider.url.replace("/chat/completions", "/models");
81
  const fetchHeaders = { "Content-Type": "application/json" };
82
 
 
 
 
83
  try {
84
  const resp = await fetch(modelsUrl, { method: "GET", headers: fetchHeaders });
85
  if (!resp.ok) return [];
 
92
 
93
  if (modelsArray.length > 0) {
94
  return modelsArray
 
95
  .filter(model => !isAudioModel(model))
 
 
 
 
 
 
 
 
 
96
  .map(model => ({
97
  ...model,
98
  id: model.id || model.name,
 
110
  results.forEach(result => {
111
  if (result.status === "fulfilled") allModels = allModels.concat(result.value);
112
  });
113
+
114
+ // Fallback: Si los proveedores gratuitos fallan al devolver la lista, inyectamos modelos estándar
115
+ if (allModels.length === 0) {
116
+ allModels = [
117
+ { id: "gpt-4o", object: "model", type: "text", owned_by: "pollinations-ai" },
118
+ { id: "claude-3-5-sonnet", object: "model", type: "text", owned_by: "pollinations-ai" }
119
+ ];
120
+ }
121
+
122
  return allModels;
123
  }
124
 
 
125
  app.get('/health', (req, res) => {
126
  res.json({
127
  status: "online",
 
131
  });
132
  });
133
 
 
134
  app.get('/v1/models', async (req, res) => {
135
  try {
136
  const allModels = await fetchAllModels();
 
137
  const textModels = allModels
138
  .filter(m => !isImageModel(m) && m.supports_chat !== false)
139
  .map(m => {
 
148
  }
149
  });
150
 
 
151
  app.get('/v1/images/models', async (req, res) => {
152
  try {
153
  const allModels = await fetchAllModels();
 
154
  let imageModels = allModels
155
  .filter(m => isImageModel(m))
156
  .map(m => {
 
176
  }
177
  });
178
 
 
179
  app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req, res) => {
180
  const isImage = req.path === '/v1/images/generations';
 
181
  let availableProviders = isImage ? PROVIDERS.filter(p => p.imageUrl) : [...PROVIDERS];
182
+
183
+ // Respaldo por si ningún proveedor declara explícitamente soportar imágenes
184
+ if (isImage && availableProviders.length === 0) availableProviders = [...PROVIDERS];
185
+
186
  const startTime = Date.now();
187
  let responseSent = false;
188
 
189
+ // Capturamos la IP real del usuario
190
+ const clientIp = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
191
+
192
  while (availableProviders.length > 0 && Date.now() - startTime < QUEUE_TIMEOUT) {
193
  let selectedProvider = null;
 
194
  let shuffled = [...availableProviders].sort(() => Math.random() - 0.5);
195
 
196
  for (let provider of shuffled) {
 
215
  };
216
 
217
  try {
218
+ let targetUrl = isImage && selectedProvider.imageUrl ? selectedProvider.imageUrl : selectedProvider.url;
 
 
 
219
 
220
+ // Reenviar la IP del usuario en las cabeceras para aislar los rate limits
221
+ const fetchHeaders = {
222
+ "Content-Type": "application/json",
223
+ "X-Forwarded-For": clientIp,
224
+ "X-Real-IP": clientIp,
225
+ "True-Client-IP": clientIp
226
+ };
227
 
228
  const response = await fetch(targetUrl, {
229
+ method: "POST",
230
  headers: fetchHeaders,
231
+ body: JSON.stringify(req.body)
232
  });
233
 
234
  if (!response.ok) {
235
+ logError(selectedProvider.id, `Fallo HTTP ${response.status} (Posible Rate Limit a la IP ${clientIp})`);
236
  releaseSlot();
237
  availableProviders = availableProviders.filter(p => p.id !== selectedProvider.id);
238
  continue;
239
  }
240
 
 
241
  const responseHeaders = new Headers(response.headers);
242
  responseHeaders.delete('set-cookie');
243
  responseHeaders.delete('server');
 
249
 
250
  if (contentType.includes("application/json")) {
251
  const jsonResp = await response.json();
 
252
  let dataArray = jsonResp.data;
253
+ if (!dataArray && jsonResp.url) dataArray = [{ url: jsonResp.url }];
 
 
254
 
255
  if (dataArray && Array.isArray(dataArray)) {
256
  for (let item of dataArray) {
 
329
  }
330
 
331
  if (!responseSent) {
332
+ logError("ProxyMain", "Todos los proveedores fallaron.");
333
+ return res.status(503).json({ error: { message: "El servicio no está disponible temporalmente.", code: 503 } });
334
  }
335
  });
336
 
337
  app.listen(PORT, '0.0.0.0', () => {
338
+ console.log(`🚀 API Proxy corriendo seguro en el puerto ${PORT}`);
339
  });