Juanoto2012 commited on
Commit
9226776
·
verified ·
1 Parent(s): 9e50e4e

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +55 -32
server.js CHANGED
@@ -6,10 +6,12 @@ import { Readable } from 'stream';
6
  const app = express();
7
  const PORT = 7860;
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
 
@@ -29,12 +31,12 @@ const PROVIDERS = [
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",
36
  url: "https://api.airforce/v1/chat/completions",
37
- imageUrl: "https://api.airforce/v1/images/generations" // Soporte de imágenes
38
  },
39
  {
40
  id: "ventarys-mirror",
@@ -48,9 +50,11 @@ const QUEUE_TIMEOUT = 25000;
48
 
49
  let currentLoad = { "llm7": 0, "pollinations": 0, "airforce": 0, "ventarys-mirror": 0 };
50
 
 
51
  const limiter = rateLimit({
52
  windowMs: 60 * 1000,
53
  max: 25,
 
54
  message: { error: { message: "Límite alcanzado. Espera 1 minuto entre mensajes.", code: 429 } },
55
  standardHeaders: true,
56
  legacyHeaders: false,
@@ -60,17 +64,19 @@ const limiter = rateLimit({
60
  const IMAGE_KEYWORDS = ["flux", "dall", "midjourney", "sdxl", "stable-diffusion", "image", "vision"];
61
  const AUDIO_KEYWORDS = ["suno", "udio", "music", "audio", "song", "voice", "tts"];
62
 
63
- function isImageModel(id) {
64
- if (!id) return false;
65
- const lowerId = id.toLowerCase();
66
- return IMAGE_KEYWORDS.some(kw => lowerId.includes(kw));
 
 
 
67
  }
68
 
69
- function isAudioModel(id, type) {
70
- if (type === 'audio' || type === 'music') return true;
71
- if (!id) return false;
72
- const lowerId = id.toLowerCase();
73
- return AUDIO_KEYWORDS.some(kw => lowerId.includes(kw));
74
  }
75
 
76
  async function fetchAllModels() {
@@ -93,8 +99,9 @@ async function fetchAllModels() {
93
 
94
  if (modelsArray.length > 0) {
95
  return modelsArray
96
- // Eliminamos los modelos de música de raíz para que no ensucien ninguna lista
97
- .filter(model => !isAudioModel(model.id || model.name, model.type))
 
98
  .map(model => ({
99
  ...model,
100
  id: model.id || model.name,
@@ -129,11 +136,14 @@ app.get('/health', (req, res) => {
129
  app.get('/v1/models', async (req, res) => {
130
  try {
131
  const allModels = await fetchAllModels();
132
- // Filtrar modelos asegurando que NO sean de imágenes
133
- const textModels = allModels.filter(m => !isImageModel(m.id) && m.type !== 'image').map(m => {
134
- m.type = 'text'; // Forzar etiqueta
135
- return m;
136
- });
 
 
 
137
 
138
  res.json({ object: "list", data: textModels });
139
  } catch (error) {
@@ -146,13 +156,16 @@ app.get('/v1/models', async (req, res) => {
146
  app.get('/v1/images/models', async (req, res) => {
147
  try {
148
  const allModels = await fetchAllModels();
149
- // Filtrar modelos asegurando que SÍ sean de imágenes
150
- let imageModels = allModels.filter(m => isImageModel(m.id) || m.type === 'image').map(m => {
151
- m.type = 'image'; // Forzar etiqueta
152
- return m;
153
- });
 
 
 
154
 
155
- // Garantizar que la App siempre reconozca los modelos base aunque la API origen no los liste
156
  const baseImages = [
157
  { id: "flux", object: "model", type: "image", owned_by: "system", tier: "standard" },
158
  { id: "dall-e-3", object: "model", type: "image", owned_by: "system", tier: "standard" }
@@ -220,14 +233,12 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
220
  const prompt = req.body.prompt || "A random image";
221
  const seed = Math.floor(Math.random() * 10000000);
222
 
223
- // Extraer resolución del body (por defecto cuadrado si no se provee)
224
  let width = 1024, height = 1024;
225
  if (req.body.size) {
226
  const parts = req.body.size.split('x');
227
  if (parts.length === 2) { width = parseInt(parts[0]); height = parseInt(parts[1]); }
228
  }
229
 
230
- // Ajustar el modelo si el usuario pidió uno en específico
231
  const pollModel = req.body.model && req.body.model.includes('flux') ? 'flux' : 'dall-e-3';
232
 
233
  targetUrl = `${targetUrl}/${encodeURIComponent(prompt)}?seed=${seed}&width=${width}&height=${height}&model=${pollModel}&nologo=true`;
@@ -253,9 +264,15 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
253
  if (contentType.includes("application/json")) {
254
  const jsonResp = await response.json();
255
 
256
- // Si la API remota nos responde con JSON, extraemos las URL para transformarlas a Base64
257
- if (jsonResp.data && Array.isArray(jsonResp.data)) {
258
- for (let item of jsonResp.data) {
 
 
 
 
 
 
259
  if (item.url && !item.b64_json) {
260
  try {
261
  const imgRes = await fetch(item.url);
@@ -267,7 +284,14 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
267
  }
268
  }
269
  }
 
 
 
 
 
270
  }
 
 
271
  releaseSlot();
272
  return res.status(response.status).json(jsonResp);
273
  }
@@ -277,14 +301,13 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
277
  const b64 = Buffer.from(arrayBuffer).toString('base64');
278
  releaseSlot();
279
 
280
- // Estandarizar respuesta para tu frontend (Aqua AI / OpenAI compatible)
281
  return res.status(200).json({
282
  created: Math.floor(Date.now() / 1000),
283
  data: [{ b64_json: b64 }]
284
  });
285
  }
286
  else {
287
- // Caída libre para errores o comportamientos inesperados de los proveedores
288
  const textResp = await response.text();
289
  releaseSlot();
290
  return res.status(response.status).type(contentType).send(textResp);
 
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
 
 
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",
39
+ imageUrl: "https://api.airforce/v1/images/generations"
40
  },
41
  {
42
  id: "ventarys-mirror",
 
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,
 
64
  const IMAGE_KEYWORDS = ["flux", "dall", "midjourney", "sdxl", "stable-diffusion", "image", "vision"];
65
  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();
73
+ return IMAGE_KEYWORDS.some(kw => id.includes(kw));
74
  }
75
 
76
+ function isAudioModel(model) {
77
+ if (model.type === 'audio' || model.type === 'music') return true;
78
+ const id = (model.id || model.name || "").toLowerCase();
79
+ return AUDIO_KEYWORDS.some(kw => id.includes(kw));
 
80
  }
81
 
82
  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,
 
136
  app.get('/v1/models', async (req, res) => {
137
  try {
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
 
148
  res.json({ object: "list", data: textModels });
149
  } catch (error) {
 
156
  app.get('/v1/images/models', async (req, res) => {
157
  try {
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" }
 
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`;
 
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);
 
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
  }
 
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);