Update server.js
Browse files
server.js
CHANGED
|
@@ -8,10 +8,12 @@ const PORT = 7860;
|
|
| 8 |
|
| 9 |
app.set('trust proxy', 1);
|
| 10 |
app.use(cors());
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
| 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://
|
| 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 |
-
|
| 93 |
}
|
| 94 |
|
| 95 |
const json = await resp.json();
|
| 96 |
let modelsArray = [];
|
| 97 |
|
| 98 |
-
if (Array.isArray(json))
|
| 99 |
-
|
| 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
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
| 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:
|
| 183 |
headers: fetchHeaders,
|
| 184 |
-
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
|
| 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;
|
| 209 |
} catch (e) {
|
| 210 |
-
logError(selectedProvider.id, `Fallo
|
| 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 |
-
//
|
| 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
|
| 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
|
| 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
|
| 263 |
-
res.status(500).json({ error:
|
| 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 |
|