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

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +108 -67
server.js CHANGED
@@ -13,7 +13,7 @@ app.use(cors());
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}`);
@@ -46,19 +46,6 @@ const PROVIDERS = [
46
  const MAX_PER_PROVIDER = 3;
47
  const QUEUE_TIMEOUT = 25000;
48
 
49
- const BLOCKED_TIERS = ["pro", "premium", "ultra", "vip", "plus", "enterprise", "max"];
50
-
51
- function isModelAllowed(modelId, modelObj = null) {
52
- if (!modelId) return true;
53
- const lowerId = modelId.toLowerCase();
54
- if (modelObj && modelObj.tier) {
55
- const tier = modelObj.tier.toLowerCase();
56
- if (tier === "pro" || tier === "premium" || tier === "vip") return false;
57
- if (tier === "free" || tier === "standard") return true;
58
- }
59
- return !BLOCKED_TIERS.some(keyword => lowerId.includes(keyword));
60
- }
61
-
62
  let currentLoad = { "llm7": 0, "pollinations": 0, "airforce": 0, "ventarys-mirror": 0 };
63
 
64
  const limiter = rateLimit({
@@ -69,30 +56,34 @@ const limiter = rateLimit({
69
  legacyHeaders: false,
70
  });
71
 
72
- // --- RUTAS INFORMATIVAS ---
73
- app.get('/health', (req, res) => {
74
- res.json({
75
- status: "online",
76
- type: "hf-node-proxy",
77
- providers: PROVIDERS.map(p => p.id),
78
- current_load: currentLoad
79
- });
80
- });
81
 
82
- app.get('/v1/models', async (req, res) => {
83
- try {
84
- const fetchPromises = PROVIDERS.map(async (provider) => {
85
- const modelsUrl = provider.modelsUrl || provider.url.replace("/chat/completions", "/models");
86
- const fetchHeaders = { "Content-Type": "application/json" };
87
-
88
- if (provider.apiKey) fetchHeaders["Authorization"] = `Bearer ${provider.apiKey}`;
89
- if (provider.proxySecret) fetchHeaders["X-Proxy-Secret"] = provider.proxySecret;
 
 
 
 
90
 
 
 
 
 
 
 
 
 
 
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 = [];
@@ -102,7 +93,8 @@ app.get('/v1/models', async (req, res) => {
102
 
103
  if (modelsArray.length > 0) {
104
  return modelsArray
105
- .filter(model => isModelAllowed(model.id || model.name, model))
 
106
  .map(model => ({
107
  ...model,
108
  id: model.id || model.name,
@@ -110,23 +102,71 @@ app.get('/v1/models', async (req, res) => {
110
  }));
111
  }
112
  return [];
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  });
114
 
115
- const results = await Promise.allSettled(fetchPromises);
116
- let allModels = [];
117
- results.forEach(result => {
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.");
130
  res.status(500).json({ error: "No se pudieron recuperar los modelos." });
131
  }
132
  });
@@ -134,22 +174,12 @@ app.get('/v1/models', async (req, res) => {
134
  // --- RUTA PRINCIPAL DE GENERACIÓN ---
135
  app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req, res) => {
136
  const isImage = req.path === '/v1/images/generations';
137
- const requestedModel = req.body.model;
138
-
139
- if (requestedModel && !isModelAllowed(requestedModel)) {
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
- });
147
- }
148
-
149
  const availableProviders = isImage ? PROVIDERS.filter(p => p.imageUrl) : PROVIDERS;
 
150
  const startTime = Date.now();
151
  let selectedProvider = null;
152
 
 
153
  while (Date.now() - startTime < QUEUE_TIMEOUT) {
154
  let shuffled = [...availableProviders].sort(() => Math.random() - 0.5);
155
  for (let provider of shuffled) {
@@ -185,12 +215,22 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
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"];
@@ -206,16 +246,16 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
206
  logError(selectedProvider.id, `Respuesta HTTP ${response.status} - ${response.statusText}`);
207
  }
208
 
209
- // --- MANEJO DE IMÁGENES (Conversión a Base64) ---
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);
@@ -231,26 +271,27 @@ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req
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();
249
  return res.status(response.status).type(contentType).send(textResp);
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',
 
13
  app.use(express.json({ limit: '50mb' }));
14
  app.use(express.urlencoded({ limit: '50mb', extended: true }));
15
 
16
+ // --- FUNCIÓN DE LOGS ---
17
  function logError(providerId, reason) {
18
  const timestamp = new Date().toISOString();
19
  console.error(`[${timestamp}] [ERROR] Proveedor: ${providerId} | Motivo: ${reason}`);
 
46
  const MAX_PER_PROVIDER = 3;
47
  const QUEUE_TIMEOUT = 25000;
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  let currentLoad = { "llm7": 0, "pollinations": 0, "airforce": 0, "ventarys-mirror": 0 };
50
 
51
  const limiter = rateLimit({
 
56
  legacyHeaders: false,
57
  });
58
 
59
+ // --- AYUDANTES PARA FILTRAR MODELOS ---
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() {
77
+ const fetchPromises = PROVIDERS.map(async (provider) => {
78
+ const modelsUrl = provider.modelsUrl || provider.url.replace("/chat/completions", "/models");
79
+ const fetchHeaders = { "Content-Type": "application/json" };
80
+
81
+ if (provider.apiKey) fetchHeaders["Authorization"] = `Bearer ${provider.apiKey}`;
82
+ if (provider.proxySecret) fetchHeaders["X-Proxy-Secret"] = provider.proxySecret;
83
+
84
+ try {
85
  const resp = await fetch(modelsUrl, { method: "GET", headers: fetchHeaders });
86
+ if (!resp.ok) return [];
 
 
 
87
 
88
  const json = await resp.json();
89
  let modelsArray = [];
 
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,
 
102
  }));
103
  }
104
  return [];
105
+ } catch (error) {
106
+ return [];
107
+ }
108
+ });
109
+
110
+ const results = await Promise.allSettled(fetchPromises);
111
+ let allModels = [];
112
+ results.forEach(result => {
113
+ if (result.status === "fulfilled") allModels = allModels.concat(result.value);
114
+ });
115
+ return allModels;
116
+ }
117
+
118
+ // --- RUTAS INFORMATIVAS ---
119
+ app.get('/health', (req, res) => {
120
+ res.json({
121
+ status: "online",
122
+ type: "hf-node-proxy",
123
+ providers: PROVIDERS.map(p => p.id),
124
+ current_load: currentLoad
125
+ });
126
+ });
127
+
128
+ // Endpoint para modelos de TEXTO
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) {
140
+ logError("ProxyMain", "Error recuperando modelos de texto.");
141
+ res.status(500).json({ error: "No se pudieron recuperar los modelos." });
142
+ }
143
+ });
144
+
145
+ // Endpoint para modelos de IMAGEN
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" }
159
+ ];
160
 
161
+ baseImages.forEach(baseMod => {
162
+ if (!imageModels.find(m => m.id.toLowerCase() === baseMod.id)) {
163
+ imageModels.push(baseMod);
164
+ }
165
+ });
166
+
167
+ res.json({ object: "list", data: imageModels });
168
  } catch (error) {
169
+ logError("ProxyMain", "Error recuperando modelos de imagen.");
170
  res.status(500).json({ error: "No se pudieron recuperar los modelos." });
171
  }
172
  });
 
174
  // --- RUTA PRINCIPAL DE GENERACIÓN ---
175
  app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req, res) => {
176
  const isImage = req.path === '/v1/images/generations';
 
 
 
 
 
 
 
 
 
 
 
 
177
  const availableProviders = isImage ? PROVIDERS.filter(p => p.imageUrl) : PROVIDERS;
178
+
179
  const startTime = Date.now();
180
  let selectedProvider = null;
181
 
182
+ // Sistema de cola inteligente
183
  while (Date.now() - startTime < QUEUE_TIMEOUT) {
184
  let shuffled = [...availableProviders].sort(() => Math.random() - 0.5);
185
  for (let provider of shuffled) {
 
215
  if (selectedProvider.apiKey) fetchHeaders["Authorization"] = `Bearer ${selectedProvider.apiKey}`;
216
  if (selectedProvider.proxySecret) fetchHeaders["X-Proxy-Secret"] = selectedProvider.proxySecret;
217
 
218
+ // Adaptador especializado para Pollinations Imágenes (Formato nativo GET)
219
  if (isImage && selectedProvider.id === "pollinations") {
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`;
234
  reqMethod = "GET";
235
  reqBody = undefined;
236
  delete fetchHeaders["Content-Type"];
 
246
  logError(selectedProvider.id, `Respuesta HTTP ${response.status} - ${response.statusText}`);
247
  }
248
 
249
+ // --- MANEJO EXCLUSIVO DE IMÁGENES (Estandarización a Base64 - Estilo Aqua AI) ---
250
  if (isImage) {
251
  const contentType = response.headers.get("content-type") || "";
252
 
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);
 
271
  releaseSlot();
272
  return res.status(response.status).json(jsonResp);
273
  }
 
274
  else if (contentType.includes("image/")) {
275
+ // Si la API nos devuelve el binario directo (Ej. Pollinations)
276
  const arrayBuffer = await response.arrayBuffer();
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);
291
  }
292
  }
293
 
294
+ // --- MANEJO EXCLUSIVO DE TEXTO (Streaming) ---
295
  res.writeHead(response.status, {
296
  'Content-Type': response.headers.get('content-type') || 'text/event-stream',
297
  'Cache-Control': 'no-cache',