Juanoto2012 commited on
Commit
41ef955
verified
1 Parent(s): cbff779

Create server.js

Browse files
Files changed (1) hide show
  1. server.js +196 -0
server.js ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import cors from 'cors';
3
+ import rateLimit from 'express-rate-limit';
4
+ import { Readable } from 'stream';
5
+
6
+ const app = express();
7
+ const PORT = 7860;
8
+
9
+ // Configuraci贸n para que funcione el l铆mite de IPs en Hugging Face Spaces
10
+ app.set('trust proxy', 1);
11
+
12
+ app.use(cors());
13
+ app.use(express.json());
14
+
15
+ // --- CONFIGURACI脫N DE PROVEEDORES ---
16
+ const PROVIDERS = [
17
+ {
18
+ id: "llm7",
19
+ url: "https://api.llm7.io/v1/chat/completions"
20
+ },
21
+ {
22
+ id: "aquadevs",
23
+ url: "https://api.aquadevs.com/v1/chat/completions",
24
+ imageUrl: "https://api.aquadevs.com/v1/images/generations",
25
+ apiKey: "aqua_sk_4f51287cffce411e8c19afacc833481d"
26
+ },
27
+ {
28
+ id: "ventarys-mirror",
29
+ url: "https://ventarys-mirror-1.hf.space/v1/chat/completions",
30
+ proxySecret: "sk-52650650a50f0v10vg150vs0v"
31
+ }
32
+ ];
33
+
34
+ const MAX_PER_PROVIDER = 3;
35
+ const QUEUE_TIMEOUT = 25000;
36
+
37
+ const BLOCKED_TIERS = ["pro", "premium", "ultra", "vip", "plus", "enterprise", "max"];
38
+
39
+ function isModelAllowed(modelId, modelObj = null) {
40
+ if (!modelId) return true;
41
+ const lowerId = modelId.toLowerCase();
42
+ if (modelObj && modelObj.tier) {
43
+ const tier = modelObj.tier.toLowerCase();
44
+ if (tier === "pro" || tier === "premium" || tier === "vip") return false;
45
+ if (tier === "free" || tier === "standard") return true;
46
+ }
47
+ return !BLOCKED_TIERS.some(keyword => lowerId.includes(keyword));
48
+ }
49
+
50
+ let currentLoad = { "llm7": 0, "aquadevs": 0, "ventarys-mirror": 0 };
51
+
52
+ // --- L脥MITE DE TASA (1 por minuto) ---
53
+ const limiter = rateLimit({
54
+ windowMs: 60 * 1000,
55
+ max: 1,
56
+ message: { error: { message: "L铆mite alcanzado. Espera 1 minuto entre mensajes.", code: 429 } },
57
+ standardHeaders: true,
58
+ legacyHeaders: false,
59
+ });
60
+
61
+ // --- RUTAS INFORMATIVAS ---
62
+ app.get('/health', (req, res) => {
63
+ res.json({
64
+ status: "online",
65
+ type: "hf-node-proxy",
66
+ providers: PROVIDERS.map(p => p.id),
67
+ current_load: currentLoad
68
+ });
69
+ });
70
+
71
+ app.get('/v1/models', async (req, res) => {
72
+ try {
73
+ const fetchPromises = PROVIDERS.map(async (provider) => {
74
+ const modelsUrl = provider.url.replace("/chat/completions", "/models");
75
+ const fetchHeaders = { "Content-Type": "application/json" };
76
+
77
+ if (provider.apiKey) fetchHeaders["Authorization"] = `Bearer ${provider.apiKey}`;
78
+ if (provider.proxySecret) fetchHeaders["X-Proxy-Secret"] = provider.proxySecret;
79
+
80
+ const resp = await fetch(modelsUrl, { method: "GET", headers: fetchHeaders });
81
+ if (!resp.ok) throw new Error(`HTTP Error ${resp.status}`);
82
+
83
+ const json = await resp.json();
84
+ if (json && Array.isArray(json.data)) {
85
+ return json.data
86
+ .filter(model => isModelAllowed(model.id, model))
87
+ .map(model => ({ ...model, owned_by: provider.id }));
88
+ }
89
+ return [];
90
+ });
91
+
92
+ const results = await Promise.allSettled(fetchPromises);
93
+ let allModels = [];
94
+ results.forEach(result => {
95
+ if (result.status === "fulfilled") allModels = allModels.concat(result.value);
96
+ });
97
+
98
+ res.json({ object: "list", data: allModels });
99
+ } catch (error) {
100
+ res.status(500).json({ error: "No se pudieron recuperar los modelos." });
101
+ }
102
+ });
103
+
104
+ // --- RUTA PRINCIPAL DE GENERACI脫N (Con L铆mite de Tasa) ---
105
+ app.post(['/v1/chat/completions', '/v1/images/generations'], limiter, async (req, res) => {
106
+ const isImage = req.path === '/v1/images/generations';
107
+ const requestedModel = req.body.model;
108
+
109
+ // Filtro de Seguridad
110
+ if (requestedModel && !isModelAllowed(requestedModel)) {
111
+ return res.status(403).json({
112
+ error: {
113
+ message: `Acceso denegado: El modelo '${requestedModel}' es de pago (Premium/Pro).`,
114
+ type: "model_not_allowed", code: 403
115
+ }
116
+ });
117
+ }
118
+
119
+ const availableProviders = isImage ? PROVIDERS.filter(p => p.imageUrl) : PROVIDERS;
120
+ const startTime = Date.now();
121
+ let selectedProvider = null;
122
+
123
+ // Sala de Espera
124
+ while (Date.now() - startTime < QUEUE_TIMEOUT) {
125
+ let shuffled = [...availableProviders].sort(() => Math.random() - 0.5);
126
+ for (let provider of shuffled) {
127
+ if (currentLoad[provider.id] < MAX_PER_PROVIDER) {
128
+ selectedProvider = provider;
129
+ currentLoad[provider.id]++;
130
+ break;
131
+ }
132
+ }
133
+ if (selectedProvider) break;
134
+ await new Promise(r => setTimeout(r, 1500));
135
+ }
136
+
137
+ if (!selectedProvider) {
138
+ return res.status(503).json({ error: { message: "Todas las APIs est谩n ocupadas. Por favor, reintenta.", code: 503 } });
139
+ }
140
+
141
+ // Funci贸n segura para liberar el Slot
142
+ let isReleased = false;
143
+ const releaseSlot = () => {
144
+ if (!isReleased) {
145
+ currentLoad[selectedProvider.id] = Math.max(0, currentLoad[selectedProvider.id] - 1);
146
+ isReleased = true;
147
+ }
148
+ };
149
+
150
+ try {
151
+ const targetUrl = isImage ? selectedProvider.imageUrl : selectedProvider.url;
152
+ const fetchHeaders = { "Content-Type": "application/json" };
153
+
154
+ if (selectedProvider.apiKey) fetchHeaders["Authorization"] = `Bearer ${selectedProvider.apiKey}`;
155
+ if (selectedProvider.proxySecret) fetchHeaders["X-Proxy-Secret"] = selectedProvider.proxySecret;
156
+
157
+ const response = await fetch(targetUrl, {
158
+ method: "POST",
159
+ headers: fetchHeaders,
160
+ body: JSON.stringify(req.body)
161
+ });
162
+
163
+ if (isImage) {
164
+ const responseData = await response.text();
165
+ releaseSlot();
166
+ return res.status(response.status).type('application/json').send(responseData);
167
+ }
168
+
169
+ // Manejo de Streaming para Chat
170
+ res.writeHead(response.status, {
171
+ 'Content-Type': response.headers.get('content-type') || 'text/event-stream',
172
+ 'Cache-Control': 'no-cache',
173
+ 'Connection': 'keep-alive'
174
+ });
175
+
176
+ if (response.body) {
177
+ const stream = Readable.fromWeb(response.body);
178
+ stream.pipe(res);
179
+
180
+ stream.on('end', releaseSlot);
181
+ stream.on('error', releaseSlot);
182
+ req.on('close', releaseSlot); // Libera si el usuario cierra la pesta帽a antes de terminar
183
+ } else {
184
+ releaseSlot();
185
+ res.end();
186
+ }
187
+
188
+ } catch (err) {
189
+ releaseSlot();
190
+ res.status(500).json({ error: `Error de conexi贸n con la API de ${selectedProvider.id}.` });
191
+ }
192
+ });
193
+
194
+ app.listen(PORT, '0.0.0.0', () => {
195
+ console.log(`馃殌 API Main Proxy (Node.js) corriendo en el puerto ${PORT}`);
196
+ });