Juanoto2012 commited on
Commit
1aade64
verified
1 Parent(s): a457776

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +75 -25
server.js CHANGED
@@ -6,12 +6,17 @@ import { Readable } from 'stream';
6
  const app = express();
7
  const PORT = 7860;
8
 
9
- // Configuraci贸n para que funcione el l铆mite de IPs en Hugging Face Spaces
10
  app.set('trust proxy', 1);
11
-
12
  app.use(cors());
13
  app.use(express.json());
14
 
 
 
 
 
 
 
 
15
  // --- CONFIGURACI脫N DE PROVEEDORES ---
16
  const PROVIDERS = [
17
  {
@@ -19,18 +24,17 @@ const PROVIDERS = [
19
  url: "https://api.llm7.io/v1/chat/completions"
20
  },
21
  {
22
- // Nuevo proveedor: Pollinations AI
23
  id: "pollinations",
24
  url: "https://text.pollinations.ai/openai",
25
- modelsUrl: "https://text.pollinations.ai/models" // Ruta espec铆fica para extraer modelos
 
26
  },
27
  {
28
- // Nuevo proveedor gratuito y sin key
29
  id: "airforce",
30
- url: "https://api.airforce/v1/chat/completions"
 
31
  },
32
  {
33
- // Tu espejo seguro
34
  id: "ventarys-mirror",
35
  url: "https://ventarys-mirror-1.hf.space/v1/chat/completions",
36
  proxySecret: "sk-52650650a50f0v10vg150vs0v"
@@ -55,8 +59,6 @@ function isModelAllowed(modelId, modelObj = null) {
55
 
56
  let currentLoad = { "llm7": 0, "pollinations": 0, "airforce": 0, "ventarys-mirror": 0 };
57
 
58
- // --- L脥MITE DE TASA (Para producci贸n en ventarys.net) ---
59
- // Te lo sub铆 a 25 peticiones por minuto para que tus usuarios no se topen con el error 429
60
  const limiter = rateLimit({
61
  windowMs: 60 * 1000,
62
  max: 25,
@@ -75,11 +77,9 @@ app.get('/health', (req, res) => {
75
  });
76
  });
77
 
78
- // --- EXTRACCI脫N Y FILTRO DE MODELOS MULTIPROVEEDOR ---
79
  app.get('/v1/models', async (req, res) => {
80
  try {
81
  const fetchPromises = PROVIDERS.map(async (provider) => {
82
- // Usa la URL espec铆fica de modelos si existe, si no, deduce la est谩ndar
83
  const modelsUrl = provider.modelsUrl || provider.url.replace("/chat/completions", "/models");
84
  const fetchHeaders = { "Content-Type": "application/json" };
85
 
@@ -87,13 +87,14 @@ app.get('/v1/models', async (req, res) => {
87
  if (provider.proxySecret) fetchHeaders["X-Proxy-Secret"] = provider.proxySecret;
88
 
89
  const resp = await fetch(modelsUrl, { method: "GET", headers: fetchHeaders });
90
- if (!resp.ok) throw new Error(`HTTP Error ${resp.status}`);
 
 
 
91
 
92
  const json = await resp.json();
93
  let modelsArray = [];
94
 
95
- // Traductor autom谩tico: Algunas APIs devuelven un array directo (Pollinations),
96
- // otras devuelven un objeto con la propiedad "data" (OpenAI Standard)
97
  if (Array.isArray(json)) {
98
  modelsArray = json;
99
  } else if (json && Array.isArray(json.data)) {
@@ -105,7 +106,7 @@ app.get('/v1/models', async (req, res) => {
105
  .filter(model => isModelAllowed(model.id || model.name, model))
106
  .map(model => ({
107
  ...model,
108
- id: model.id || model.name, // Asegura que tu frontend siempre vea un "id"
109
  owned_by: provider.id
110
  }));
111
  }
@@ -120,6 +121,7 @@ app.get('/v1/models', async (req, res) => {
120
 
121
  res.json({ object: "list", data: allModels });
122
  } catch (error) {
 
123
  res.status(500).json({ error: "No se pudieron recuperar los modelos." });
124
  }
125
  });
@@ -129,8 +131,8 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
129
  const isImage = req.path === '/v1/images/generations';
130
  const requestedModel = req.body.model;
131
 
132
- // Filtro de Seguridad de Tiers
133
  if (requestedModel && !isModelAllowed(requestedModel)) {
 
134
  return res.status(403).json({
135
  error: {
136
  message: `Acceso denegado: El modelo '${requestedModel}' es de pago (Premium/Pro).`,
@@ -143,7 +145,6 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
143
  const startTime = Date.now();
144
  let selectedProvider = null;
145
 
146
- // Sala de Espera / Balanceo
147
  while (Date.now() - startTime < QUEUE_TIMEOUT) {
148
  let shuffled = [...availableProviders].sort(() => Math.random() - 0.5);
149
  for (let provider of shuffled) {
@@ -158,10 +159,10 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
158
  }
159
 
160
  if (!selectedProvider) {
 
161
  return res.status(503).json({ error: { message: "Todas las APIs est谩n ocupadas. Por favor, reintenta.", code: 503 } });
162
  }
163
 
164
- // Funci贸n segura para liberar el Slot
165
  let isReleased = false;
166
  const releaseSlot = () => {
167
  if (!isReleased) {
@@ -183,13 +184,58 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
183
  body: JSON.stringify(req.body)
184
  });
185
 
 
 
 
 
 
 
186
  if (isImage) {
187
- const responseData = await response.text();
188
- releaseSlot();
189
- return res.status(response.status).type('application/json').send(responseData);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
  }
191
 
192
- // Manejo de Streaming para Chat
193
  res.writeHead(response.status, {
194
  'Content-Type': response.headers.get('content-type') || 'text/event-stream',
195
  'Cache-Control': 'no-cache',
@@ -201,7 +247,10 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
201
  stream.pipe(res);
202
 
203
  stream.on('end', releaseSlot);
204
- stream.on('error', releaseSlot);
 
 
 
205
  req.on('close', releaseSlot);
206
  } else {
207
  releaseSlot();
@@ -210,10 +259,11 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
210
 
211
  } catch (err) {
212
  releaseSlot();
213
- res.status(500).json({ error: `Error de conexi贸n con la API de ${selectedProvider.id}.` });
 
214
  }
215
  });
216
 
217
  app.listen(PORT, '0.0.0.0', () => {
218
- console.log(`馃殌 API Main Proxy (Node.js) corriendo en el puerto ${PORT}`);
219
  });
 
6
  const app = express();
7
  const PORT = 7860;
8
 
 
9
  app.set('trust proxy', 1);
 
10
  app.use(cors());
11
  app.use(express.json());
12
 
13
+ // --- FUNCI脫N DE LOGS (RESPETANDO PRIVACIDAD) ---
14
+ // Solo se llama cuando algo falla, omitiendo IPs y mensajes (prompts).
15
+ function logError(providerId, reason) {
16
+ const timestamp = new Date().toISOString();
17
+ console.error(`[${timestamp}] [ERROR] Proveedor: ${providerId} | Motivo: ${reason}`);
18
+ }
19
+
20
  // --- CONFIGURACI脫N DE PROVEEDORES ---
21
  const PROVIDERS = [
22
  {
 
24
  url: "https://api.llm7.io/v1/chat/completions"
25
  },
26
  {
 
27
  id: "pollinations",
28
  url: "https://text.pollinations.ai/openai",
29
+ modelsUrl: "https://text.pollinations.ai/models",
30
+ imageUrl: "https://text.pollinations.ai/openai" // Soporte compatible
31
  },
32
  {
 
33
  id: "airforce",
34
+ url: "https://api.airforce/v1/chat/completions",
35
+ imageUrl: "https://api.airforce/v1/images/generations" // Soporte de im谩genes
36
  },
37
  {
 
38
  id: "ventarys-mirror",
39
  url: "https://ventarys-mirror-1.hf.space/v1/chat/completions",
40
  proxySecret: "sk-52650650a50f0v10vg150vs0v"
 
59
 
60
  let currentLoad = { "llm7": 0, "pollinations": 0, "airforce": 0, "ventarys-mirror": 0 };
61
 
 
 
62
  const limiter = rateLimit({
63
  windowMs: 60 * 1000,
64
  max: 25,
 
77
  });
78
  });
79
 
 
80
  app.get('/v1/models', async (req, res) => {
81
  try {
82
  const fetchPromises = PROVIDERS.map(async (provider) => {
 
83
  const modelsUrl = provider.modelsUrl || provider.url.replace("/chat/completions", "/models");
84
  const fetchHeaders = { "Content-Type": "application/json" };
85
 
 
87
  if (provider.proxySecret) fetchHeaders["X-Proxy-Secret"] = provider.proxySecret;
88
 
89
  const resp = await fetch(modelsUrl, { method: "GET", headers: fetchHeaders });
90
+ if (!resp.ok) {
91
+ logError(provider.id, `Fallo al recuperar modelos (HTTP ${resp.status})`);
92
+ throw new Error(`HTTP Error ${resp.status}`);
93
+ }
94
 
95
  const json = await resp.json();
96
  let modelsArray = [];
97
 
 
 
98
  if (Array.isArray(json)) {
99
  modelsArray = json;
100
  } else if (json && Array.isArray(json.data)) {
 
106
  .filter(model => isModelAllowed(model.id || model.name, model))
107
  .map(model => ({
108
  ...model,
109
+ id: model.id || model.name,
110
  owned_by: provider.id
111
  }));
112
  }
 
121
 
122
  res.json({ object: "list", data: allModels });
123
  } catch (error) {
124
+ logError("ProxyMain", "Error cr铆tico al agrupar la lista de modelos.");
125
  res.status(500).json({ error: "No se pudieron recuperar los modelos." });
126
  }
127
  });
 
131
  const isImage = req.path === '/v1/images/generations';
132
  const requestedModel = req.body.model;
133
 
 
134
  if (requestedModel && !isModelAllowed(requestedModel)) {
135
+ logError("ProxyMain", `Bloqueado intento de uso de modelo premium: ${requestedModel}`);
136
  return res.status(403).json({
137
  error: {
138
  message: `Acceso denegado: El modelo '${requestedModel}' es de pago (Premium/Pro).`,
 
145
  const startTime = Date.now();
146
  let selectedProvider = null;
147
 
 
148
  while (Date.now() - startTime < QUEUE_TIMEOUT) {
149
  let shuffled = [...availableProviders].sort(() => Math.random() - 0.5);
150
  for (let provider of shuffled) {
 
159
  }
160
 
161
  if (!selectedProvider) {
162
+ logError("ProxyMain", "Saturaci贸n - Todas las APIs est谩n ocupadas (Cola llena)");
163
  return res.status(503).json({ error: { message: "Todas las APIs est谩n ocupadas. Por favor, reintenta.", code: 503 } });
164
  }
165
 
 
166
  let isReleased = false;
167
  const releaseSlot = () => {
168
  if (!isReleased) {
 
184
  body: JSON.stringify(req.body)
185
  });
186
 
187
+ // Registrar si la API del proveedor devuelve un error (sin registrar el prompt)
188
+ if (!response.ok) {
189
+ logError(selectedProvider.id, `Respuesta HTTP ${response.status} - ${response.statusText}`);
190
+ }
191
+
192
+ // --- MANEJO DE IM脕GENES (Conversi贸n a Base64) ---
193
  if (isImage) {
194
+ const contentType = response.headers.get("content-type") || "";
195
+
196
+ // Caso 1: Devuelve un JSON est谩ndar (Posiblemente con URL en lugar de b64_json)
197
+ if (contentType.includes("application/json")) {
198
+ const jsonResp = await response.json();
199
+
200
+ if (jsonResp.data && Array.isArray(jsonResp.data)) {
201
+ for (let item of jsonResp.data) {
202
+ // Si la API nos devolvi贸 una URL en vez de base64, la convertimos nosotros
203
+ if (item.url && !item.b64_json) {
204
+ try {
205
+ const imgRes = await fetch(item.url);
206
+ const arrayBuffer = await imgRes.arrayBuffer();
207
+ item.b64_json = Buffer.from(arrayBuffer).toString('base64');
208
+ delete item.url; // Quitamos la URL para dejar tu app limpia
209
+ } catch (e) {
210
+ logError(selectedProvider.id, `Fallo al convertir la URL de imagen a Base64: ${e.message}`);
211
+ }
212
+ }
213
+ }
214
+ }
215
+ releaseSlot();
216
+ return res.status(response.status).json(jsonResp);
217
+ }
218
+ // Caso 2: Devuelve la imagen cruda (Ej: Pollinations genera binarios directos a veces)
219
+ else if (contentType.includes("image/")) {
220
+ const arrayBuffer = await response.arrayBuffer();
221
+ const b64 = Buffer.from(arrayBuffer).toString('base64');
222
+ releaseSlot();
223
+
224
+ // Lo empaquetamos como si fuera un JSON est谩ndar de OpenAI
225
+ return res.status(200).json({
226
+ created: Math.floor(Date.now() / 1000),
227
+ data: [{ b64_json: b64 }]
228
+ });
229
+ }
230
+ // Caso 3: Fallo catastr贸fico u otro texto
231
+ else {
232
+ const textResp = await response.text();
233
+ releaseSlot();
234
+ return res.status(response.status).type(contentType).send(textResp);
235
+ }
236
  }
237
 
238
+ // --- MANEJO DE CHAT (Streaming) ---
239
  res.writeHead(response.status, {
240
  'Content-Type': response.headers.get('content-type') || 'text/event-stream',
241
  'Cache-Control': 'no-cache',
 
247
  stream.pipe(res);
248
 
249
  stream.on('end', releaseSlot);
250
+ stream.on('error', (err) => {
251
+ logError(selectedProvider.id, `Stream roto a mitad de la respuesta: ${err.message}`);
252
+ releaseSlot();
253
+ });
254
  req.on('close', releaseSlot);
255
  } else {
256
  releaseSlot();
 
259
 
260
  } catch (err) {
261
  releaseSlot();
262
+ logError(selectedProvider.id, `Fallo de red o Timeout conectando al proveedor: ${err.message}`);
263
+ res.status(500).json({ error: `Error de conexi贸n interna.` });
264
  }
265
  });
266
 
267
  app.listen(PORT, '0.0.0.0', () => {
268
+ console.log(`馃殌 API Proxy (Node.js) corriendo seguro en el puerto ${PORT}`);
269
  });