opex792 commited on
Commit
6e08682
·
verified ·
1 Parent(s): 5469ff7

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockefile +15 -0
  2. package.json +21 -0
  3. server.js +564 -0
Dockefile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-slim
2
+
3
+ WORKDIR /usr/src/app
4
+
5
+ COPY package*.json ./
6
+
7
+ RUN npm install --omit=dev
8
+
9
+ COPY . .
10
+
11
+ EXPOSE 7860
12
+
13
+ ENV NODE_ENV=production
14
+
15
+ CMD ["node", "server.js"]
package.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "gemini-proxy-rotator",
3
+ "version": "1.0.0",
4
+ "description": "An OpenAI-compatible proxy rotator for Gemini API keys.",
5
+ "main": "server.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "start": "node server.js"
9
+ },
10
+ "dependencies": {
11
+ "axios": "^1.6.8",
12
+ "express": "^4.19.2",
13
+ "express-fileupload": "^1.5.0",
14
+ "sequelize": "^6.37.3",
15
+ "sqlite3": "^5.1.7",
16
+ "uuid": "^9.0.1"
17
+ },
18
+ "devDependencies": {
19
+ "nodemon": "^3.1.0"
20
+ }
21
+ }
server.js ADDED
@@ -0,0 +1,564 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import { Sequelize, DataTypes, Op } from 'sequelize';
3
+ import axios from 'axios';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+ import fileUpload from 'express-fileupload';
6
+ import path from 'path';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const PORT = process.env.PORT || 7860;
13
+ const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
14
+
15
+ if (!ADMIN_PASSWORD) {
16
+ console.error("CRITICAL ERROR: The ADMIN_PASSWORD environment variable is not set. Set it in your Hugging Face Space secrets.");
17
+ process.exit(1);
18
+ }
19
+
20
+ const DB_PATH = path.join(__dirname, 'database.sqlite');
21
+ const KEY_DEACTIVATION_THRESHOLD = 5;
22
+ const KEY_COOLDOWN_SECONDS = 60;
23
+
24
+ const app = express();
25
+ app.use(express.json({ limit: '10mb' }));
26
+ app.use(express.urlencoded({ extended: true }));
27
+ app.use(fileUpload());
28
+
29
+ const sequelize = new Sequelize({
30
+ dialect: 'sqlite',
31
+ storage: DB_PATH,
32
+ logging: false
33
+ });
34
+
35
+ const GeminiKey = sequelize.define('GeminiKey', {
36
+ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
37
+ key: { type: DataTypes.STRING, allowNull: false, unique: true },
38
+ is_active: { type: DataTypes.BOOLEAN, defaultValue: true },
39
+ last_used_at: { type: DataTypes.DATE },
40
+ error_count: { type: DataTypes.INTEGER, defaultValue: 0 },
41
+ last_error: { type: DataTypes.STRING },
42
+ cooldown_until: { type: DataTypes.DATE, defaultValue: null }
43
+ }, { timestamps: true });
44
+
45
+ const RequestLog = sequelize.define('RequestLog', {
46
+ id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
47
+ gemini_key_id: { type: DataTypes.INTEGER, references: { model: GeminiKey, key: 'id' } },
48
+ service_key_id: { type: DataTypes.STRING },
49
+ model_requested: { type: DataTypes.STRING },
50
+ request_body: { type: DataTypes.TEXT },
51
+ response_body: { type: DataTypes.TEXT },
52
+ status_code: { type: DataTypes.INTEGER },
53
+ is_success: { type: DataTypes.BOOLEAN },
54
+ error_message: { type: DataTypes.TEXT },
55
+ processing_time_ms: { type: DataTypes.INTEGER },
56
+ prompt_tokens: { type: DataTypes.INTEGER, defaultValue: 0 },
57
+ completion_tokens: { type: DataTypes.INTEGER, defaultValue: 0 },
58
+ total_tokens: { type: DataTypes.INTEGER, defaultValue: 0 }
59
+ }, { timestamps: true });
60
+
61
+ const ServiceKey = sequelize.define('ServiceKey', {
62
+ key: { type: DataTypes.STRING, primaryKey: true, defaultValue: () => `sk-gemini-proxy-${uuidv4()}` },
63
+ owner: { type: DataTypes.STRING, allowNull: false },
64
+ is_active: { type: DataTypes.BOOLEAN, defaultValue: true },
65
+ rpm_limit: { type: DataTypes.INTEGER, defaultValue: 15 },
66
+ rpd_limit: { type: DataTypes.INTEGER, defaultValue: 1500 },
67
+ tpm_limit: { type: DataTypes.INTEGER, defaultValue: 1000000 },
68
+ tpd_limit: { type: DataTypes.INTEGER, defaultValue: 2000000 },
69
+ }, { timestamps: true });
70
+
71
+ GeminiKey.hasMany(RequestLog, { foreignKey: 'gemini_key_id' });
72
+ RequestLog.belongsTo(GeminiKey, { foreignKey: 'gemini_key_id' });
73
+
74
+ const GEMINI_DEFAULT_LIMITS = {
75
+ 'gemini-2.5-flash-preview-05-20': { rpm: 10, rpd: 500, tpm: 250000, tpd: 500000 },
76
+ 'gemini-2.5-flash-preview-tts': { rpm: 3, rpd: 15, tpm: 10000, tpd: 20000 },
77
+ 'gemini-2.5-pro-experimental-03-25': { rpm: 5, rpd: 25, tpm: 250000, tpd: 1000000 },
78
+ 'gemini-2.0-flash': { rpm: 15, rpd: 1500, tpm: 1000000, tpd: 2000000 },
79
+ 'gemini-2.0-flash-experimental': { rpm: 10, rpd: 1000, tpm: 250000, tpd: 500000 },
80
+ 'gemini-2.0-flash-lite': { rpm: 30, rpd: 1500, tpm: 1000000, tpd: 2000000 },
81
+ 'gemini-1.5-flash': { rpm: 15, rpd: 500, tpm: 250000, tpd: 500000 },
82
+ 'gemini-1.5-flash-8b': { rpm: 15, rpd: 500, tpm: 250000, tpd: 500000 },
83
+ 'gemma-3': { rpm: 30, rpd: 14400, tpm: 15000, tpd: 360000 },
84
+ 'gemma-3n': { rpm: 30, rpd: 14400, tpm: 15000, tpd: 360000 },
85
+ 'default': { rpm: 15, rpd: 1500, tpm: 1000000, tpd: 2000000 }
86
+ };
87
+
88
+ function getModelLimits(model) {
89
+ return GEMINI_DEFAULT_LIMITS[model] || GEMINI_DEFAULT_LIMITS['default'];
90
+ }
91
+
92
+ function safeParseInt(value, defaultValue = 0) {
93
+ const parsed = Number(value);
94
+ return isNaN(parsed) ? defaultValue : Math.floor(parsed);
95
+ }
96
+
97
+ async function selectBestGeminiKey(model, attemptedKeys = new Set()) {
98
+ const now = new Date();
99
+ const oneMinuteAgo = new Date(now.getTime() - 60 * 1000);
100
+ const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
101
+
102
+ const activeKeys = await GeminiKey.findAll({
103
+ where: {
104
+ is_active: true,
105
+ id: { [Op.notIn]: [...attemptedKeys] },
106
+ [Op.or]: [
107
+ { cooldown_until: null },
108
+ { cooldown_until: { [Op.lt]: now } }
109
+ ]
110
+ },
111
+ order: [['last_used_at', 'ASC']]
112
+ });
113
+
114
+ if (activeKeys.length === 0) return null;
115
+
116
+ const keyUsageStats = await RequestLog.findAll({
117
+ attributes: [
118
+ 'gemini_key_id',
119
+ [sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneMinuteAgo THEN 1 ELSE 0 END`)), 'requestsLastMinute'],
120
+ [sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneDayAgo THEN 1 ELSE 0 END`)), 'requestsLastDay'],
121
+ [sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneMinuteAgo THEN COALESCE(total_tokens, 0) ELSE 0 END`)), 'tokensLastMinute'],
122
+ [sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneDayAgo THEN COALESCE(total_tokens, 0) ELSE 0 END`)), 'tokensLastDay'],
123
+ ],
124
+ where: {
125
+ is_success: true,
126
+ gemini_key_id: { [Op.in]: activeKeys.map(k => k.id) },
127
+ createdAt: { [Op.gte]: oneDayAgo }
128
+ },
129
+ group: ['gemini_key_id'],
130
+ replacements: { oneMinuteAgo, oneDayAgo }
131
+ });
132
+
133
+ const usageMap = keyUsageStats.reduce((acc, usage) => {
134
+ const data = usage.get();
135
+ acc[data.gemini_key_id] = {
136
+ rpm: safeParseInt(data.requestsLastMinute),
137
+ rpd: safeParseInt(data.requestsLastDay),
138
+ tpm: safeParseInt(data.tokensLastMinute),
139
+ tpd: safeParseInt(data.tokensLastDay),
140
+ };
141
+ return acc;
142
+ }, {});
143
+
144
+ let bestKey = null;
145
+ let minUsagePercentage = Infinity;
146
+
147
+ for (const key of activeKeys) {
148
+ const limits = getModelLimits(model);
149
+ const usage = usageMap[key.id] || { rpm: 0, rpd: 0, tpm: 0, tpd: 0 };
150
+
151
+ const rpmUsage = (usage.rpm / limits.rpm) * 100;
152
+ const rpdUsage = (usage.rpd / limits.rpd) * 100;
153
+ const tpmUsage = (usage.tpm / limits.tpm) * 100;
154
+ const tpdUsage = (usage.tpd / limits.tpd) * 100;
155
+
156
+ const maxUsageForThisKey = Math.max(rpmUsage, rpdUsage, tpmUsage, tpdUsage);
157
+
158
+ if (maxUsageForThisKey < minUsagePercentage) {
159
+ minUsagePercentage = maxUsageForThisKey;
160
+ bestKey = key;
161
+ }
162
+ }
163
+
164
+ return bestKey || activeKeys[0];
165
+ }
166
+
167
+ const authenticateServiceKey = async (req, res, next) => {
168
+ const authHeader = req.headers.authorization;
169
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
170
+ return res.status(401).json({ error: 'Authorization header is missing or invalid. Expected: Bearer YOUR_SERVICE_KEY' });
171
+ }
172
+ const key = authHeader.split(' ')[1];
173
+ const serviceKey = await ServiceKey.findByPk(key);
174
+
175
+ if (!serviceKey || !serviceKey.is_active) {
176
+ return res.status(403).json({ error: 'Invalid or inactive service key.' });
177
+ }
178
+ req.serviceKey = serviceKey;
179
+
180
+ const now = new Date();
181
+ const oneMinuteAgo = new Date(now.getTime() - 60 * 1000);
182
+ const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
183
+
184
+ const usageResult = await RequestLog.findOne({
185
+ attributes: [
186
+ [sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneMinuteAgo THEN 1 ELSE 0 END`)), 'requestsLastMinute'],
187
+ [sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneDayAgo THEN 1 ELSE 0 END`)), 'requestsLastDay'],
188
+ [sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneMinuteAgo THEN COALESCE(total_tokens, 0) ELSE 0 END`)), 'tokensLastMinute'],
189
+ [sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneDayAgo THEN COALESCE(total_tokens, 0) ELSE 0 END`)), 'tokensLastDay']
190
+ ],
191
+ where: {
192
+ service_key_id: key,
193
+ createdAt: { [Op.gte]: oneDayAgo }
194
+ },
195
+ replacements: { oneMinuteAgo, oneDayAgo }
196
+ });
197
+
198
+ const usage = {
199
+ requestsLastMinute: safeParseInt(usageResult?.get('requestsLastMinute')),
200
+ requestsLastDay: safeParseInt(usageResult?.get('requestsLastDay')),
201
+ tokensLastMinute: safeParseInt(usageResult?.get('tokensLastMinute')),
202
+ tokensLastDay: safeParseInt(usageResult?.get('tokensLastDay')),
203
+ };
204
+
205
+ if (usage.requestsLastMinute >= serviceKey.rpm_limit) {
206
+ return res.status(429).json({ error: `Rate limit exceeded. Your RPM limit is ${serviceKey.rpm_limit}.` });
207
+ }
208
+ if (usage.tokensLastMinute >= serviceKey.tpm_limit) {
209
+ return res.status(429).json({ error: `Token limit exceeded. Your TPM limit is ${serviceKey.tpm_limit}.` });
210
+ }
211
+ if (usage.requestsLastDay >= serviceKey.rpd_limit) {
212
+ return res.status(429).json({ error: `Daily request limit exceeded. Your RPD limit is ${serviceKey.rpd_limit}.` });
213
+ }
214
+ if (usage.tokensLastDay >= serviceKey.tpd_limit) {
215
+ return res.status(429).json({ error: `Daily token limit exceeded. Your TPD limit is ${serviceKey.tpd_limit}.` });
216
+ }
217
+
218
+ next();
219
+ };
220
+
221
+ const adminAuth = (req, res, next) => {
222
+ const authHeader = req.headers.authorization;
223
+ if (!authHeader || authHeader !== `Bearer ${ADMIN_PASSWORD}`) {
224
+ return res.status(401).send('Unauthorized: Invalid admin credentials.');
225
+ }
226
+ next();
227
+ };
228
+
229
+ app.post('/v1/chat/completions', authenticateServiceKey, async (req, res) => {
230
+ if (!req.body || typeof req.body !== 'object') {
231
+ return res.status(400).json({ error: 'Request body must be a valid JSON object.' });
232
+ }
233
+
234
+ const startTime = Date.now();
235
+ const model = req.body.model;
236
+ if (!model) {
237
+ return res.status(400).json({ error: 'The "model" field is required in the request body.' });
238
+ }
239
+
240
+ let geminiKey = null;
241
+ const maxAttempts = await GeminiKey.count({ where: { is_active: true } });
242
+ const attemptedKeys = new Set();
243
+ let lastError = null;
244
+ let lastStatusCode = 500;
245
+
246
+ while (attemptedKeys.size < maxAttempts) {
247
+ geminiKey = await selectBestGeminiKey(model, attemptedKeys);
248
+ if (!geminiKey) break;
249
+
250
+ attemptedKeys.add(geminiKey.id);
251
+
252
+ try {
253
+ const geminiApiUrl = `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions`;
254
+ const response = await axios.post(geminiApiUrl, req.body, {
255
+ headers: {
256
+ 'Content-Type': 'application/json',
257
+ 'Authorization': `Bearer ${geminiKey.key}`
258
+ },
259
+ timeout: 45000
260
+ });
261
+
262
+ const responseData = response.data;
263
+ geminiKey.error_count = 0;
264
+ geminiKey.cooldown_until = null;
265
+ geminiKey.last_used_at = new Date();
266
+ await geminiKey.save();
267
+
268
+ await RequestLog.create({
269
+ gemini_key_id: geminiKey.id,
270
+ service_key_id: req.serviceKey.key,
271
+ model_requested: model,
272
+ request_body: JSON.stringify(req.body),
273
+ response_body: JSON.stringify(responseData),
274
+ status_code: response.status,
275
+ is_success: true,
276
+ processing_time_ms: Date.now() - startTime,
277
+ prompt_tokens: responseData.usage?.prompt_tokens || 0,
278
+ completion_tokens: responseData.usage?.completion_tokens || 0,
279
+ total_tokens: responseData.usage?.total_tokens || 0,
280
+ });
281
+
282
+ return res.status(response.status).json(responseData);
283
+
284
+ } catch (error) {
285
+ const isTimeout = error.code === 'ECONNABORTED';
286
+ const errorMessage = isTimeout ? 'Request timed out' : (error.response ? JSON.stringify(error.response.data) : error.message);
287
+ const statusCode = isTimeout ? 504 : (error.response ? error.response.status : 500);
288
+
289
+ lastError = errorMessage;
290
+ lastStatusCode = statusCode;
291
+
292
+ await RequestLog.create({
293
+ gemini_key_id: geminiKey ? geminiKey.id : null,
294
+ service_key_id: req.serviceKey.key,
295
+ model_requested: model,
296
+ request_body: JSON.stringify(req.body),
297
+ status_code: statusCode,
298
+ is_success: false,
299
+ error_message: errorMessage,
300
+ processing_time_ms: Date.now() - startTime
301
+ });
302
+
303
+ if (geminiKey) {
304
+ geminiKey.error_count += 1;
305
+ geminiKey.last_error = errorMessage.substring(0, 255);
306
+
307
+ if (statusCode === 429) {
308
+ geminiKey.cooldown_until = new Date(Date.now() + KEY_COOLDOWN_SECONDS * 1000);
309
+ } else if (statusCode === 400 || statusCode === 403 || geminiKey.error_count >= KEY_DEACTIVATION_THRESHOLD) {
310
+ geminiKey.is_active = false;
311
+ }
312
+ await geminiKey.save();
313
+ }
314
+ }
315
+ }
316
+
317
+ let errorDetails;
318
+ try {
319
+ errorDetails = lastError ? JSON.parse(lastError) : { message: 'No error details available.' };
320
+ } catch (e) {
321
+ errorDetails = { raw_error: lastError };
322
+ }
323
+
324
+ const finalError = {
325
+ error: "Failed to process request. All available Gemini keys resulted in errors.",
326
+ details: errorDetails
327
+ };
328
+ if (lastStatusCode === 429) {
329
+ finalError.error = "Service is overloaded. All available Gemini keys have reached their rate limits. Please try again later.";
330
+ }
331
+ return res.status(lastStatusCode).json(finalError);
332
+ });
333
+
334
+ app.get('/admin', adminAuth, (req, res) => {
335
+ res.send(`
336
+ <html>
337
+ <head><title>Admin Panel</title><meta name="viewport" content="width=device-width, initial-scale=1"></head>
338
+ <body style="font-family: sans-serif; padding: 20px; max-width: 800px; margin: auto;">
339
+ <h1>Gemini Proxy Admin Panel</h1>
340
+ <h2>Upload Gemini Keys</h2>
341
+ <p>Upload a .txt file with one Gemini API key per line.</p>
342
+ <form action="/admin/upload-keys" method="post" enctype="multipart/form-data">
343
+ <input type="file" name="keysFile" accept=".txt" required>
344
+ <button type="submit">Upload</button>
345
+ </form>
346
+ <hr>
347
+ <h2>Manage Service Keys</h2>
348
+ <form action="/admin/service-keys" method="post" target="creation-result">
349
+ <input type="text" name="owner" placeholder="Owner (e.g., user@example.com)" required style="width: 250px; padding: 5px;">
350
+ <button type="submit">Create New Service Key</button>
351
+ </form>
352
+ <iframe name="creation-result" style="width: 100%; height: 50px; border: 1px solid #ccc; margin-top: 10px;"></iframe>
353
+ <hr>
354
+ <h2>Stats & Data</h2>
355
+ <ul>
356
+ <li><a href="/admin/stats" target="_blank">View Stats (JSON)</a></li>
357
+ <li><a href="/admin/download/geminikeys">Download GeminiKeys Table (JSON)</a></li>
358
+ <li><a href="/admin/download/requestlogs">Download RequestLogs Table (JSON)</a></li>
359
+ <li><a href="/admin/download/servicekeys">Download ServiceKeys Table (JSON)</a></li>
360
+ </ul>
361
+ </body>
362
+ </html>
363
+ `);
364
+ });
365
+
366
+ app.post('/admin/upload-keys', adminAuth, async (req, res) => {
367
+ if (!req.files || !req.files.keysFile) {
368
+ return res.status(400).send('No files were uploaded.');
369
+ }
370
+ const keysFile = req.files.keysFile;
371
+ const keys = keysFile.data.toString('utf8').split(/\r?\n/).filter(key => key.trim() !== '');
372
+
373
+ let newKeysCount = 0;
374
+ let failedKeys = [];
375
+ for (const key of keys) {
376
+ const trimmedKey = key.trim();
377
+ if (!trimmedKey) continue;
378
+ try {
379
+ const [_, created] = await GeminiKey.findOrCreate({
380
+ where: { key: trimmedKey },
381
+ defaults: { key: trimmedKey }
382
+ });
383
+ if (created) newKeysCount++;
384
+ } catch (error) {
385
+ failedKeys.push(trimmedKey);
386
+ }
387
+ }
388
+ res.send(`File processed successfully. Added ${newKeysCount} new unique keys. Failed to add ${failedKeys.length} keys (likely duplicates).`);
389
+ });
390
+
391
+ app.post('/admin/service-keys', adminAuth, async (req, res) => {
392
+ const { owner } = req.body;
393
+ if (!owner) return res.status(400).send('Owner is required.');
394
+ try {
395
+ const newKey = await ServiceKey.create({ owner });
396
+ res.status(201).send(`<b>Key created successfully!</b><br><code>${newKey.key}</code>`);
397
+ } catch (error) {
398
+ res.status(500).json({ message: "Failed to create service key", error: error.message });
399
+ }
400
+ });
401
+
402
+ app.get('/admin/stats', adminAuth, async (req, res) => {
403
+ const totalGeminiKeys = await GeminiKey.count();
404
+ const activeGeminiKeys = await GeminiKey.count({ where: { is_active: true } });
405
+ const totalRequests = await RequestLog.count();
406
+ const successfulRequests = await RequestLog.count({ where: { is_success: true } });
407
+ const totalServiceKeys = await ServiceKey.count();
408
+
409
+ res.json({
410
+ gemini_keys: {
411
+ total: totalGeminiKeys,
412
+ active: activeGeminiKeys,
413
+ inactive: totalGeminiKeys - activeGeminiKeys,
414
+ },
415
+ requests: {
416
+ total: totalRequests,
417
+ successful: successfulRequests,
418
+ failed: totalRequests - successfulRequests,
419
+ success_rate: totalRequests > 0 ? ((successfulRequests / totalRequests) * 100).toFixed(2) + '%' : 'N/A',
420
+ },
421
+ service_keys_total: totalServiceKeys,
422
+ });
423
+ });
424
+
425
+ app.get('/admin/download/:table', adminAuth, async (req, res) => {
426
+ const { table } = req.params;
427
+ const modelMap = {
428
+ 'geminikeys': GeminiKey,
429
+ 'requestlogs': RequestLog,
430
+ 'servicekeys': ServiceKey
431
+ };
432
+ const model = modelMap[table.toLowerCase()];
433
+ if (!model) return res.status(404).send('Table not found');
434
+
435
+ try {
436
+ const data = await model.findAll({ order: [['createdAt', 'DESC']] });
437
+ res.header('Content-Type', 'application/json');
438
+ res.header('Content-Disposition', `attachment; filename=${table}.json`);
439
+ res.send(JSON.stringify(data, null, 2));
440
+ } catch (error) {
441
+ res.status(500).send(`Error fetching data for table ${table}: ${error.message}`);
442
+ }
443
+ });
444
+
445
+ app.get('/user/dashboard', authenticateServiceKey, async (req, res) => {
446
+ const oneDayAgo = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
447
+ const usage = await RequestLog.findOne({
448
+ attributes: [
449
+ [sequelize.fn('COUNT', sequelize.col('id')), 'requests'],
450
+ [sequelize.fn('SUM', sequelize.col('total_tokens')), 'tokens']
451
+ ],
452
+ where: {
453
+ service_key_id: req.serviceKey.key,
454
+ createdAt: { [Op.gte]: oneDayAgo }
455
+ }
456
+ });
457
+
458
+ const requestsLastDay = usage?.get('requests') || 0;
459
+ const tokensLastDay = usage?.get('tokens') || 0;
460
+
461
+ res.send(`
462
+ <html>
463
+ <head><title>User Dashboard</title><meta name="viewport" content="width=device-width, initial-scale=1"></head>
464
+ <body style="font-family: sans-serif; padding: 20px; max-width: 800px; margin: auto;">
465
+ <h1>User Dashboard</h1>
466
+ <p><b>Owner:</b> ${req.serviceKey.owner}</p>
467
+ <p><b>Your Service Key:</b> <code style="background: #eee; padding: 2px 5px; border-radius: 4px;">${req.serviceKey.key}</code></p>
468
+ <h3>Your Daily Usage & Limits (last 24h)</h3>
469
+ <p><b>Requests:</b> ${requestsLastDay} / ${req.serviceKey.rpd_limit}</p>
470
+ <p><b>Tokens:</b> ${tokensLastDay} / ${req.serviceKey.tpd_limit}</p>
471
+ <h3>Your Rate Limits</h3>
472
+ <ul>
473
+ <li><b>Requests per minute:</b> ${req.serviceKey.rpm_limit}</li>
474
+ <li><b>Requests per day:</b> ${req.serviceKey.rpd_limit}</li>
475
+ <li><b>Tokens per minute:</b> ${req.serviceKey.tpm_limit}</li>
476
+ <li><b>Tokens per day:</b> ${req.serviceKey.tpd_limit}</li>
477
+ </ul>
478
+ <hr>
479
+ <h3>API Usage</h3>
480
+ <p>Use this service key in your requests:</p>
481
+ <pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;">curl "http://localhost:7860/v1/chat/completions" \\
482
+ -H "Content-Type: application/json" \\
483
+ -H "Authorization: Bearer ${req.serviceKey.key}" \\
484
+ -d '{
485
+ "model": "gemini-2.0-flash",
486
+ "messages": [
487
+ {"role": "user", "content": "Hello!"}
488
+ ]
489
+ }'</pre>
490
+ </body>
491
+ </html>
492
+ `);
493
+ });
494
+
495
+ app.get('/user/stats', authenticateServiceKey, async (req, res) => {
496
+ const now = new Date();
497
+ const oneMinuteAgo = new Date(now.getTime() - 60 * 1000);
498
+ const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
499
+
500
+ const usageResult = await RequestLog.findOne({
501
+ attributes: [
502
+ [sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneMinuteAgo THEN 1 ELSE 0 END`)), 'requestsLastMinute'],
503
+ [sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneDayAgo THEN 1 ELSE 0 END`)), 'requestsLastDay'],
504
+ [sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneMinuteAgo THEN COALESCE(total_tokens, 0) ELSE 0 END`)), 'tokensLastMinute'],
505
+ [sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneDayAgo THEN COALESCE(total_tokens, 0) ELSE 0 END`)), 'tokensLastDay']
506
+ ],
507
+ where: {
508
+ service_key_id: req.serviceKey.key,
509
+ createdAt: { [Op.gte]: oneDayAgo }
510
+ },
511
+ replacements: { oneMinuteAgo, oneDayAgo }
512
+ });
513
+
514
+ const usage = {
515
+ requestsLastMinute: safeParseInt(usageResult?.get('requestsLastMinute')),
516
+ requestsLastDay: safeParseInt(usageResult?.get('requestsLastDay')),
517
+ tokensLastMinute: safeParseInt(usageResult?.get('tokensLastMinute')),
518
+ tokensLastDay: safeParseInt(usageResult?.get('tokensLastDay')),
519
+ };
520
+
521
+ res.json({
522
+ service_key: req.serviceKey.key,
523
+ owner: req.serviceKey.owner,
524
+ limits: {
525
+ rpm: req.serviceKey.rpm_limit,
526
+ rpd: req.serviceKey.rpd_limit,
527
+ tpm: req.serviceKey.tpm_limit,
528
+ tpd: req.serviceKey.tpd_limit
529
+ },
530
+ current_usage: usage,
531
+ usage_percentages: {
532
+ rpm: ((usage.requestsLastMinute / req.serviceKey.rpm_limit) * 100).toFixed(2) + '%',
533
+ rpd: ((usage.requestsLastDay / req.serviceKey.rpd_limit) * 100).toFixed(2) + '%',
534
+ tpm: ((usage.tokensLastMinute / req.serviceKey.tpm_limit) * 100).toFixed(2) + '%',
535
+ tpd: ((usage.tokensLastDay / req.serviceKey.tpd_limit) * 100).toFixed(2) + '%'
536
+ }
537
+ });
538
+ });
539
+
540
+ app.get('/v1/models', authenticateServiceKey, async (req, res) => {
541
+ const models = Object.keys(GEMINI_DEFAULT_LIMITS).filter(model => model !== 'default').map(model => ({
542
+ id: model,
543
+ object: "model",
544
+ created: 1677649963,
545
+ owned_by: "google"
546
+ }));
547
+
548
+ res.json({
549
+ object: "list",
550
+ data: models
551
+ });
552
+ });
553
+
554
+ sequelize.sync({ force: false }).then(() => {
555
+ console.log('Database initialized successfully.');
556
+ app.listen(PORT, () => {
557
+ console.log(`Gemini Proxy Rotator running on port ${PORT}`);
558
+ console.log(`Admin panel: http://localhost:${PORT}/admin`);
559
+ console.log(`Set ADMIN_PASSWORD environment variable for admin access`);
560
+ });
561
+ }).catch(error => {
562
+ console.error('Failed to initialize database:', error);
563
+ process.exit(1);
564
+ });