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

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +41 -26
server.js CHANGED
@@ -8,10 +8,12 @@ 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}`);
@@ -27,7 +29,7 @@ const PROVIDERS = [
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",
@@ -89,17 +91,14 @@ app.get('/v1/models', async (req, res) => {
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)) {
101
- modelsArray = json.data;
102
- }
103
 
104
  if (modelsArray.length > 0) {
105
  return modelsArray
@@ -119,6 +118,12 @@ app.get('/v1/models', async (req, res) => {
119
  if (result.status === "fulfilled") allModels = allModels.concat(result.value);
120
  });
121
 
 
 
 
 
 
 
122
  res.json({ object: "list", data: allModels });
123
  } catch (error) {
124
  logError("ProxyMain", "Error crítico al agrupar la lista de modelos.");
@@ -135,7 +140,7 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
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).`,
139
  type: "model_not_allowed", code: 403
140
  }
141
  });
@@ -159,7 +164,7 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
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
 
@@ -172,19 +177,31 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
172
  };
173
 
174
  try {
175
- const targetUrl = isImage ? selectedProvider.imageUrl : selectedProvider.url;
 
 
176
  const fetchHeaders = { "Content-Type": "application/json" };
177
 
178
  if (selectedProvider.apiKey) fetchHeaders["Authorization"] = `Bearer ${selectedProvider.apiKey}`;
179
  if (selectedProvider.proxySecret) fetchHeaders["X-Proxy-Secret"] = selectedProvider.proxySecret;
180
 
 
 
 
 
 
 
 
 
 
 
 
181
  const response = await fetch(targetUrl, {
182
- method: "POST",
183
  headers: fetchHeaders,
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
  }
@@ -193,21 +210,20 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
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
  }
@@ -215,19 +231,18 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
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();
@@ -235,7 +250,7 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
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',
@@ -248,7 +263,7 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
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);
@@ -259,8 +274,8 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
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
 
 
8
 
9
  app.set('trust proxy', 1);
10
  app.use(cors());
11
+
12
+ // CRÍTICO: Aumentar límite a 50mb para soportar imágenes en Base64 (Visión / Multimodal)
13
+ app.use(express.json({ limit: '50mb' }));
14
+ app.use(express.urlencoded({ limit: '50mb', extended: true }));
15
 
16
  // --- FUNCIÓN DE LOGS (RESPETANDO PRIVACIDAD) ---
 
17
  function logError(providerId, reason) {
18
  const timestamp = new Date().toISOString();
19
  console.error(`[${timestamp}] [ERROR] Proveedor: ${providerId} | Motivo: ${reason}`);
 
29
  id: "pollinations",
30
  url: "https://text.pollinations.ai/openai",
31
  modelsUrl: "https://text.pollinations.ai/models",
32
+ imageUrl: "https://image.pollinations.ai/prompt" // Endpoint nativo de imágenes
33
  },
34
  {
35
  id: "airforce",
 
91
  const resp = await fetch(modelsUrl, { method: "GET", headers: fetchHeaders });
92
  if (!resp.ok) {
93
  logError(provider.id, `Fallo al recuperar modelos (HTTP ${resp.status})`);
94
+ return [];
95
  }
96
 
97
  const json = await resp.json();
98
  let modelsArray = [];
99
 
100
+ if (Array.isArray(json)) modelsArray = json;
101
+ else if (json && Array.isArray(json.data)) modelsArray = json.data;
 
 
 
102
 
103
  if (modelsArray.length > 0) {
104
  return modelsArray
 
118
  if (result.status === "fulfilled") allModels = allModels.concat(result.value);
119
  });
120
 
121
+ // Garantizar que la App siempre reconozca modelos de imágenes base
122
+ allModels.push(
123
+ { id: "flux", object: "model", type: "image", owned_by: "system" },
124
+ { id: "dall-e-3", object: "model", type: "image", owned_by: "system" }
125
+ );
126
+
127
  res.json({ object: "list", data: allModels });
128
  } catch (error) {
129
  logError("ProxyMain", "Error crítico al agrupar la lista de modelos.");
 
140
  logError("ProxyMain", `Bloqueado intento de uso de modelo premium: ${requestedModel}`);
141
  return res.status(403).json({
142
  error: {
143
+ message: `Acceso denegado: El modelo '${requestedModel}' es de pago.`,
144
  type: "model_not_allowed", code: 403
145
  }
146
  });
 
164
  }
165
 
166
  if (!selectedProvider) {
167
+ logError("ProxyMain", "Saturación - Todas las APIs están ocupadas");
168
  return res.status(503).json({ error: { message: "Todas las APIs están ocupadas. Por favor, reintenta.", code: 503 } });
169
  }
170
 
 
177
  };
178
 
179
  try {
180
+ let targetUrl = isImage ? selectedProvider.imageUrl : selectedProvider.url;
181
+ let reqMethod = "POST";
182
+ let reqBody = JSON.stringify(req.body);
183
  const fetchHeaders = { "Content-Type": "application/json" };
184
 
185
  if (selectedProvider.apiKey) fetchHeaders["Authorization"] = `Bearer ${selectedProvider.apiKey}`;
186
  if (selectedProvider.proxySecret) fetchHeaders["X-Proxy-Secret"] = selectedProvider.proxySecret;
187
 
188
+ // Adaptador para Pollinations Imágenes (Usa URL GET en lugar de un Payload POST)
189
+ if (isImage && selectedProvider.id === "pollinations") {
190
+ const prompt = req.body.prompt || "A random image";
191
+ const seed = Math.floor(Math.random() * 10000000);
192
+ // Redirige al enpoint puro para obtener la imagen
193
+ targetUrl = `${targetUrl}/${encodeURIComponent(prompt)}?seed=${seed}&nologo=true`;
194
+ reqMethod = "GET";
195
+ reqBody = undefined;
196
+ delete fetchHeaders["Content-Type"];
197
+ }
198
+
199
  const response = await fetch(targetUrl, {
200
+ method: reqMethod,
201
  headers: fetchHeaders,
202
+ body: reqBody
203
  });
204
 
 
205
  if (!response.ok) {
206
  logError(selectedProvider.id, `Respuesta HTTP ${response.status} - ${response.statusText}`);
207
  }
 
210
  if (isImage) {
211
  const contentType = response.headers.get("content-type") || "";
212
 
 
213
  if (contentType.includes("application/json")) {
214
  const jsonResp = await response.json();
215
 
216
  if (jsonResp.data && Array.isArray(jsonResp.data)) {
217
  for (let item of jsonResp.data) {
218
+ // Si nos devuelve URL en vez de base64, lo descargamos y convertimos para la app
219
  if (item.url && !item.b64_json) {
220
  try {
221
  const imgRes = await fetch(item.url);
222
  const arrayBuffer = await imgRes.arrayBuffer();
223
  item.b64_json = Buffer.from(arrayBuffer).toString('base64');
224
+ delete item.url;
225
  } catch (e) {
226
+ logError(selectedProvider.id, `Fallo convirtiendo URL a Base64: ${e.message}`);
227
  }
228
  }
229
  }
 
231
  releaseSlot();
232
  return res.status(response.status).json(jsonResp);
233
  }
234
+ // Manejo de la respuesta nativa de imágenes (Pollinations)
235
  else if (contentType.includes("image/")) {
236
  const arrayBuffer = await response.arrayBuffer();
237
  const b64 = Buffer.from(arrayBuffer).toString('base64');
238
  releaseSlot();
239
 
240
+ // Lo empaquetamos exactamente como tu frontend (OpenAI standard) lo espera
241
  return res.status(200).json({
242
  created: Math.floor(Date.now() / 1000),
243
  data: [{ b64_json: b64 }]
244
  });
245
  }
 
246
  else {
247
  const textResp = await response.text();
248
  releaseSlot();
 
250
  }
251
  }
252
 
253
+ // --- MANEJO DE CHAT TEXTO (Streaming) ---
254
  res.writeHead(response.status, {
255
  'Content-Type': response.headers.get('content-type') || 'text/event-stream',
256
  'Cache-Control': 'no-cache',
 
263
 
264
  stream.on('end', releaseSlot);
265
  stream.on('error', (err) => {
266
+ logError(selectedProvider.id, `Stream roto a mitad de respuesta: ${err.message}`);
267
  releaseSlot();
268
  });
269
  req.on('close', releaseSlot);
 
274
 
275
  } catch (err) {
276
  releaseSlot();
277
+ logError(selectedProvider ? selectedProvider.id : "Proxy", `Fallo o Timeout conectando al proveedor: ${err.message}`);
278
+ res.status(500).json({ error: { message: "Error de conexión interna."} });
279
  }
280
  });
281