api / server.js
Juanoto2012's picture
Update server.js
fbdbcb7 verified
raw
history blame
11 kB
import express from 'express';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import { Readable } from 'stream';
const app = express();
const PORT = 7860;
app.set('trust proxy', 1);
app.use(cors());
// CR脥TICO: Aumentar l铆mite a 50mb para soportar im谩genes en Base64 (Visi贸n / Multimodal)
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ limit: '50mb', extended: true }));
// --- FUNCI脫N DE LOGS (RESPETANDO PRIVACIDAD) ---
function logError(providerId, reason) {
const timestamp = new Date().toISOString();
console.error(`[${timestamp}] [ERROR] Proveedor: ${providerId} | Motivo: ${reason}`);
}
// --- CONFIGURACI脫N DE PROVEEDORES ---
const PROVIDERS = [
{
id: "llm7",
url: "https://api.llm7.io/v1/chat/completions"
},
{
id: "pollinations",
url: "https://text.pollinations.ai/openai",
modelsUrl: "https://text.pollinations.ai/models",
imageUrl: "https://image.pollinations.ai/prompt" // Endpoint nativo de im谩genes
},
{
id: "airforce",
url: "https://api.airforce/v1/chat/completions",
imageUrl: "https://api.airforce/v1/images/generations" // Soporte de im谩genes
},
{
id: "ventarys-mirror",
url: "https://ventarys-mirror-1.hf.space/v1/chat/completions",
proxySecret: "sk-52650650a50f0v10vg150vs0v"
}
];
const MAX_PER_PROVIDER = 3;
const QUEUE_TIMEOUT = 25000;
const BLOCKED_TIERS = ["pro", "premium", "ultra", "vip", "plus", "enterprise", "max"];
function isModelAllowed(modelId, modelObj = null) {
if (!modelId) return true;
const lowerId = modelId.toLowerCase();
if (modelObj && modelObj.tier) {
const tier = modelObj.tier.toLowerCase();
if (tier === "pro" || tier === "premium" || tier === "vip") return false;
if (tier === "free" || tier === "standard") return true;
}
return !BLOCKED_TIERS.some(keyword => lowerId.includes(keyword));
}
let currentLoad = { "llm7": 0, "pollinations": 0, "airforce": 0, "ventarys-mirror": 0 };
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 25,
message: { error: { message: "L铆mite alcanzado. Espera 1 minuto entre mensajes.", code: 429 } },
standardHeaders: true,
legacyHeaders: false,
});
// --- RUTAS INFORMATIVAS ---
app.get('/health', (req, res) => {
res.json({
status: "online",
type: "hf-node-proxy",
providers: PROVIDERS.map(p => p.id),
current_load: currentLoad
});
});
app.get('/v1/models', async (req, res) => {
try {
const fetchPromises = PROVIDERS.map(async (provider) => {
const modelsUrl = provider.modelsUrl || provider.url.replace("/chat/completions", "/models");
const fetchHeaders = { "Content-Type": "application/json" };
if (provider.apiKey) fetchHeaders["Authorization"] = `Bearer ${provider.apiKey}`;
if (provider.proxySecret) fetchHeaders["X-Proxy-Secret"] = provider.proxySecret;
const resp = await fetch(modelsUrl, { method: "GET", headers: fetchHeaders });
if (!resp.ok) {
logError(provider.id, `Fallo al recuperar modelos (HTTP ${resp.status})`);
return [];
}
const json = await resp.json();
let modelsArray = [];
if (Array.isArray(json)) modelsArray = json;
else if (json && Array.isArray(json.data)) modelsArray = json.data;
if (modelsArray.length > 0) {
return modelsArray
.filter(model => isModelAllowed(model.id || model.name, model))
.map(model => ({
...model,
id: model.id || model.name,
owned_by: provider.id
}));
}
return [];
});
const results = await Promise.allSettled(fetchPromises);
let allModels = [];
results.forEach(result => {
if (result.status === "fulfilled") allModels = allModels.concat(result.value);
});
// Garantizar que la App siempre reconozca modelos de im谩genes base
allModels.push(
{ id: "flux", object: "model", type: "image", owned_by: "system" },
{ id: "dall-e-3", object: "model", type: "image", owned_by: "system" }
);
res.json({ object: "list", data: allModels });
} catch (error) {
logError("ProxyMain", "Error cr铆tico al agrupar la lista de modelos.");
res.status(500).json({ error: "No se pudieron recuperar los modelos." });
}
});
// --- RUTA PRINCIPAL DE GENERACI脫N ---
app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req, res) => {
const isImage = req.path === '/v1/images/generations';
const requestedModel = req.body.model;
if (requestedModel && !isModelAllowed(requestedModel)) {
logError("ProxyMain", `Bloqueado intento de uso de modelo premium: ${requestedModel}`);
return res.status(403).json({
error: {
message: `Acceso denegado: El modelo '${requestedModel}' es de pago.`,
type: "model_not_allowed", code: 403
}
});
}
const availableProviders = isImage ? PROVIDERS.filter(p => p.imageUrl) : PROVIDERS;
const startTime = Date.now();
let selectedProvider = null;
while (Date.now() - startTime < QUEUE_TIMEOUT) {
let shuffled = [...availableProviders].sort(() => Math.random() - 0.5);
for (let provider of shuffled) {
if (currentLoad[provider.id] < MAX_PER_PROVIDER) {
selectedProvider = provider;
currentLoad[provider.id]++;
break;
}
}
if (selectedProvider) break;
await new Promise(r => setTimeout(r, 1500));
}
if (!selectedProvider) {
logError("ProxyMain", "Saturaci贸n - Todas las APIs est谩n ocupadas");
return res.status(503).json({ error: { message: "Todas las APIs est谩n ocupadas. Por favor, reintenta.", code: 503 } });
}
let isReleased = false;
const releaseSlot = () => {
if (!isReleased) {
currentLoad[selectedProvider.id] = Math.max(0, currentLoad[selectedProvider.id] - 1);
isReleased = true;
}
};
try {
let targetUrl = isImage ? selectedProvider.imageUrl : selectedProvider.url;
let reqMethod = "POST";
let reqBody = JSON.stringify(req.body);
const fetchHeaders = { "Content-Type": "application/json" };
if (selectedProvider.apiKey) fetchHeaders["Authorization"] = `Bearer ${selectedProvider.apiKey}`;
if (selectedProvider.proxySecret) fetchHeaders["X-Proxy-Secret"] = selectedProvider.proxySecret;
// Adaptador para Pollinations Im谩genes (Usa URL GET en lugar de un Payload POST)
if (isImage && selectedProvider.id === "pollinations") {
const prompt = req.body.prompt || "A random image";
const seed = Math.floor(Math.random() * 10000000);
// Redirige al enpoint puro para obtener la imagen
targetUrl = `${targetUrl}/${encodeURIComponent(prompt)}?seed=${seed}&nologo=true`;
reqMethod = "GET";
reqBody = undefined;
delete fetchHeaders["Content-Type"];
}
const response = await fetch(targetUrl, {
method: reqMethod,
headers: fetchHeaders,
body: reqBody
});
if (!response.ok) {
logError(selectedProvider.id, `Respuesta HTTP ${response.status} - ${response.statusText}`);
}
// --- MANEJO DE IM脕GENES (Conversi贸n a Base64) ---
if (isImage) {
const contentType = response.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
const jsonResp = await response.json();
if (jsonResp.data && Array.isArray(jsonResp.data)) {
for (let item of jsonResp.data) {
// Si nos devuelve URL en vez de base64, lo descargamos y convertimos para la app
if (item.url && !item.b64_json) {
try {
const imgRes = await fetch(item.url);
const arrayBuffer = await imgRes.arrayBuffer();
item.b64_json = Buffer.from(arrayBuffer).toString('base64');
delete item.url;
} catch (e) {
logError(selectedProvider.id, `Fallo convirtiendo URL a Base64: ${e.message}`);
}
}
}
}
releaseSlot();
return res.status(response.status).json(jsonResp);
}
// Manejo de la respuesta nativa de im谩genes (Pollinations)
else if (contentType.includes("image/")) {
const arrayBuffer = await response.arrayBuffer();
const b64 = Buffer.from(arrayBuffer).toString('base64');
releaseSlot();
// Lo empaquetamos exactamente como tu frontend (OpenAI standard) lo espera
return res.status(200).json({
created: Math.floor(Date.now() / 1000),
data: [{ b64_json: b64 }]
});
}
else {
const textResp = await response.text();
releaseSlot();
return res.status(response.status).type(contentType).send(textResp);
}
}
// --- MANEJO DE CHAT TEXTO (Streaming) ---
res.writeHead(response.status, {
'Content-Type': response.headers.get('content-type') || 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
});
if (response.body) {
const stream = Readable.fromWeb(response.body);
stream.pipe(res);
stream.on('end', releaseSlot);
stream.on('error', (err) => {
logError(selectedProvider.id, `Stream roto a mitad de respuesta: ${err.message}`);
releaseSlot();
});
req.on('close', releaseSlot);
} else {
releaseSlot();
res.end();
}
} catch (err) {
releaseSlot();
logError(selectedProvider ? selectedProvider.id : "Proxy", `Fallo o Timeout conectando al proveedor: ${err.message}`);
res.status(500).json({ error: { message: "Error de conexi贸n interna."} });
}
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`馃殌 API Proxy (Node.js) corriendo seguro en el puerto ${PORT}`);
});