Update server.js
Browse files
server.js
CHANGED
|
@@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
|
|
| 5 |
import fileUpload from 'express-fileupload';
|
| 6 |
import path from 'path';
|
| 7 |
import { fileURLToPath } from 'url';
|
| 8 |
-
import fs from 'fs';
|
| 9 |
|
| 10 |
const __filename = fileURLToPath(import.meta.url);
|
| 11 |
const __dirname = path.dirname(__filename);
|
|
@@ -18,10 +18,15 @@ if (!ADMIN_PASSWORD) {
|
|
| 18 |
process.exit(1);
|
| 19 |
}
|
| 20 |
|
| 21 |
-
//
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
const KEY_DEACTIVATION_THRESHOLD = 5;
|
| 26 |
const KEY_COOLDOWN_SECONDS = 60;
|
| 27 |
|
|
@@ -30,11 +35,6 @@ app.use(express.json({ limit: '10mb' }));
|
|
| 30 |
app.use(express.urlencoded({ extended: true }));
|
| 31 |
app.use(fileUpload());
|
| 32 |
|
| 33 |
-
// Ensure the database directory exists before initializing Sequelize
|
| 34 |
-
if (!fs.existsSync(DB_DIR)) {
|
| 35 |
-
fs.mkdirSync(DB_DIR, { recursive: true });
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
const sequelize = new Sequelize({
|
| 39 |
dialect: 'sqlite',
|
| 40 |
storage: DB_PATH,
|
|
@@ -81,21 +81,16 @@ GeminiKey.hasMany(RequestLog, { foreignKey: 'gemini_key_id' });
|
|
| 81 |
RequestLog.belongsTo(GeminiKey, { foreignKey: 'gemini_key_id' });
|
| 82 |
|
| 83 |
const GEMINI_DEFAULT_LIMITS = {
|
| 84 |
-
'gemini-
|
| 85 |
-
'gemini-
|
| 86 |
-
'gemini-
|
| 87 |
-
'gemini-2.0-flash': { rpm: 15, rpd: 1500, tpm: 1000000, tpd: 2000000 },
|
| 88 |
-
'gemini-2.0-flash-experimental': { rpm: 10, rpd: 1000, tpm: 250000, tpd: 500000 },
|
| 89 |
-
'gemini-2.0-flash-lite': { rpm: 30, rpd: 1500, tpm: 1000000, tpd: 2000000 },
|
| 90 |
-
'gemini-1.5-flash': { rpm: 15, rpd: 500, tpm: 250000, tpd: 500000 },
|
| 91 |
-
'gemini-1.5-flash-8b': { rpm: 15, rpd: 500, tpm: 250000, tpd: 500000 },
|
| 92 |
-
'gemma-3': { rpm: 30, rpd: 14400, tpm: 15000, tpd: 360000 },
|
| 93 |
-
'gemma-3n': { rpm: 30, rpd: 14400, tpm: 15000, tpd: 360000 },
|
| 94 |
'default': { rpm: 15, rpd: 1500, tpm: 1000000, tpd: 2000000 }
|
| 95 |
};
|
| 96 |
|
| 97 |
function getModelLimits(model) {
|
| 98 |
-
|
|
|
|
|
|
|
| 99 |
}
|
| 100 |
|
| 101 |
function safeParseInt(value, defaultValue = 0) {
|
|
@@ -235,7 +230,7 @@ const adminAuth = (req, res, next) => {
|
|
| 235 |
next();
|
| 236 |
};
|
| 237 |
|
| 238 |
-
app.post('/v1/chat/completions', authenticateServiceKey, async (req, res) => {
|
| 239 |
if (!req.body || typeof req.body !== 'object') {
|
| 240 |
return res.status(400).json({ error: 'Request body must be a valid JSON object.' });
|
| 241 |
}
|
|
@@ -259,16 +254,52 @@ app.post('/v1/chat/completions', authenticateServiceKey, async (req, res) => {
|
|
| 259 |
attemptedKeys.add(geminiKey.id);
|
| 260 |
|
| 261 |
try {
|
| 262 |
-
const geminiApiUrl = `https://generativelanguage.googleapis.com/v1beta/
|
| 263 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 264 |
headers: {
|
| 265 |
'Content-Type': 'application/json',
|
| 266 |
-
'
|
| 267 |
},
|
| 268 |
timeout: 45000
|
| 269 |
});
|
| 270 |
|
| 271 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
geminiKey.error_count = 0;
|
| 273 |
geminiKey.cooldown_until = null;
|
| 274 |
geminiKey.last_used_at = new Date();
|
|
@@ -279,16 +310,16 @@ app.post('/v1/chat/completions', authenticateServiceKey, async (req, res) => {
|
|
| 279 |
service_key_id: req.serviceKey.key,
|
| 280 |
model_requested: model,
|
| 281 |
request_body: JSON.stringify(req.body),
|
| 282 |
-
response_body: JSON.stringify(
|
| 283 |
status_code: response.status,
|
| 284 |
is_success: true,
|
| 285 |
processing_time_ms: Date.now() - startTime,
|
| 286 |
-
prompt_tokens:
|
| 287 |
-
completion_tokens:
|
| 288 |
-
total_tokens:
|
| 289 |
});
|
| 290 |
|
| 291 |
-
return res.status(response.status).json(
|
| 292 |
|
| 293 |
} catch (error) {
|
| 294 |
const isTimeout = error.code === 'ECONNABORTED';
|
|
@@ -453,19 +484,21 @@ app.get('/admin/download/:table', adminAuth, async (req, res) => {
|
|
| 453 |
|
| 454 |
app.get('/user/dashboard', authenticateServiceKey, async (req, res) => {
|
| 455 |
const oneDayAgo = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
|
| 456 |
-
const
|
| 457 |
attributes: [
|
| 458 |
[sequelize.fn('COUNT', sequelize.col('id')), 'requests'],
|
| 459 |
[sequelize.fn('SUM', sequelize.col('total_tokens')), 'tokens']
|
| 460 |
],
|
| 461 |
where: {
|
| 462 |
service_key_id: req.serviceKey.key,
|
| 463 |
-
createdAt: { [Op.gte]: oneDayAgo }
|
| 464 |
-
|
|
|
|
|
|
|
| 465 |
});
|
| 466 |
|
| 467 |
-
const requestsLastDay =
|
| 468 |
-
const tokensLastDay =
|
| 469 |
|
| 470 |
res.send(`
|
| 471 |
<html>
|
|
@@ -485,13 +518,12 @@ app.get('/user/dashboard', authenticateServiceKey, async (req, res) => {
|
|
| 485 |
<li><b>Tokens per day:</b> ${req.serviceKey.tpd_limit}</li>
|
| 486 |
</ul>
|
| 487 |
<hr>
|
| 488 |
-
<h3>API Usage</h3>
|
| 489 |
-
<
|
| 490 |
-
<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;">curl "http://localhost:7860/v1/chat/completions" \\
|
| 491 |
-H "Content-Type: application/json" \\
|
| 492 |
-H "Authorization: Bearer ${req.serviceKey.key}" \\
|
| 493 |
-d '{
|
| 494 |
-
"model": "gemini-
|
| 495 |
"messages": [
|
| 496 |
{"role": "user", "content": "Hello!"}
|
| 497 |
]
|
|
@@ -515,7 +547,8 @@ app.get('/user/stats', authenticateServiceKey, async (req, res) => {
|
|
| 515 |
],
|
| 516 |
where: {
|
| 517 |
service_key_id: req.serviceKey.key,
|
| 518 |
-
createdAt: { [Op.gte]: oneDayAgo }
|
|
|
|
| 519 |
},
|
| 520 |
replacements: { oneMinuteAgo, oneDayAgo }
|
| 521 |
});
|
|
@@ -546,11 +579,11 @@ app.get('/user/stats', authenticateServiceKey, async (req, res) => {
|
|
| 546 |
});
|
| 547 |
});
|
| 548 |
|
| 549 |
-
app.get('/v1/models', authenticateServiceKey, async (req, res) => {
|
| 550 |
const models = Object.keys(GEMINI_DEFAULT_LIMITS).filter(model => model !== 'default').map(model => ({
|
| 551 |
id: model,
|
| 552 |
object: "model",
|
| 553 |
-
created:
|
| 554 |
owned_by: "google"
|
| 555 |
}));
|
| 556 |
|
|
@@ -562,10 +595,8 @@ app.get('/v1/models', authenticateServiceKey, async (req, res) => {
|
|
| 562 |
|
| 563 |
sequelize.sync({ force: false }).then(() => {
|
| 564 |
console.log('Database initialized successfully.');
|
| 565 |
-
app.listen(PORT, () => {
|
| 566 |
console.log(`Gemini Proxy Rotator running on port ${PORT}`);
|
| 567 |
-
console.log(`Admin panel: http://localhost:${PORT}/admin`);
|
| 568 |
-
console.log(`Set ADMIN_PASSWORD environment variable for admin access`);
|
| 569 |
});
|
| 570 |
}).catch(error => {
|
| 571 |
console.error('Failed to initialize database:', error);
|
|
|
|
| 5 |
import fileUpload from 'express-fileupload';
|
| 6 |
import path from 'path';
|
| 7 |
import { fileURLToPath } from 'url';
|
| 8 |
+
import fs from 'fs';
|
| 9 |
|
| 10 |
const __filename = fileURLToPath(import.meta.url);
|
| 11 |
const __dirname = path.dirname(__filename);
|
|
|
|
| 18 |
process.exit(1);
|
| 19 |
}
|
| 20 |
|
| 21 |
+
// --- ИЗМЕНЕНИЕ ---
|
| 22 |
+
// Путь к базе данных теперь указывает на папку 'data'
|
| 23 |
+
const dataDir = path.join(__dirname, 'data');
|
| 24 |
+
if (!fs.existsSync(dataDir)) {
|
| 25 |
+
fs.mkdirSync(dataDir, { recursive: true });
|
| 26 |
+
}
|
| 27 |
+
const DB_PATH = path.join(dataDir, 'database.sqlite');
|
| 28 |
+
// --- КОНЕЦ ИЗМЕНЕНИЯ ---
|
| 29 |
+
|
| 30 |
const KEY_DEACTIVATION_THRESHOLD = 5;
|
| 31 |
const KEY_COOLDOWN_SECONDS = 60;
|
| 32 |
|
|
|
|
| 35 |
app.use(express.urlencoded({ extended: true }));
|
| 36 |
app.use(fileUpload());
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
const sequelize = new Sequelize({
|
| 39 |
dialect: 'sqlite',
|
| 40 |
storage: DB_PATH,
|
|
|
|
| 81 |
RequestLog.belongsTo(GeminiKey, { foreignKey: 'gemini_key_id' });
|
| 82 |
|
| 83 |
const GEMINI_DEFAULT_LIMITS = {
|
| 84 |
+
'gemini-1.5-flash-latest': { rpm: 15, rpd: 500, tpm: 250000, tpd: 500000 },
|
| 85 |
+
'gemini-1.5-pro-latest': { rpm: 5, rpd: 25, tpm: 250000, tpd: 1000000 },
|
| 86 |
+
'gemini-1.0-pro': { rpm: 15, rpd: 1500, tpm: 1000000, tpd: 2000000 },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
'default': { rpm: 15, rpd: 1500, tpm: 1000000, tpd: 2000000 }
|
| 88 |
};
|
| 89 |
|
| 90 |
function getModelLimits(model) {
|
| 91 |
+
// Ищем точное совпадение или совпадение по началу строки
|
| 92 |
+
const foundModel = Object.keys(GEMINI_DEFAULT_LIMITS).find(k => model.startsWith(k));
|
| 93 |
+
return GEMINI_DEFAULT_LIMITS[foundModel] || GEMINI_DEFAULT_LIMITS['default'];
|
| 94 |
}
|
| 95 |
|
| 96 |
function safeParseInt(value, defaultValue = 0) {
|
|
|
|
| 230 |
next();
|
| 231 |
};
|
| 232 |
|
| 233 |
+
app.post(['/v1/chat/completions', '/v1beta/chat/completions'], authenticateServiceKey, async (req, res) => {
|
| 234 |
if (!req.body || typeof req.body !== 'object') {
|
| 235 |
return res.status(400).json({ error: 'Request body must be a valid JSON object.' });
|
| 236 |
}
|
|
|
|
| 254 |
attemptedKeys.add(geminiKey.id);
|
| 255 |
|
| 256 |
try {
|
| 257 |
+
const geminiApiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
|
| 258 |
+
|
| 259 |
+
// OpenAI to Gemini Translation
|
| 260 |
+
const geminiRequestBody = {
|
| 261 |
+
contents: req.body.messages.map(msg => ({
|
| 262 |
+
role: msg.role === 'assistant' ? 'model' : msg.role,
|
| 263 |
+
parts: [{ text: msg.content }]
|
| 264 |
+
})),
|
| 265 |
+
generationConfig: {
|
| 266 |
+
temperature: req.body.temperature,
|
| 267 |
+
topP: req.body.top_p,
|
| 268 |
+
maxOutputTokens: req.body.max_tokens,
|
| 269 |
+
}
|
| 270 |
+
};
|
| 271 |
+
|
| 272 |
+
const response = await axios.post(geminiApiUrl, geminiRequestBody, {
|
| 273 |
headers: {
|
| 274 |
'Content-Type': 'application/json',
|
| 275 |
+
'x-goog-api-key': geminiKey.key
|
| 276 |
},
|
| 277 |
timeout: 45000
|
| 278 |
});
|
| 279 |
|
| 280 |
+
const geminiResponse = response.data;
|
| 281 |
+
|
| 282 |
+
// Gemini to OpenAI Translation
|
| 283 |
+
const openAIResponse = {
|
| 284 |
+
id: `chatcmpl-${uuidv4()}`,
|
| 285 |
+
object: 'chat.completion',
|
| 286 |
+
created: Math.floor(Date.now() / 1000),
|
| 287 |
+
model: model,
|
| 288 |
+
choices: geminiResponse.candidates.map((candidate, index) => ({
|
| 289 |
+
index: index,
|
| 290 |
+
message: {
|
| 291 |
+
role: 'assistant',
|
| 292 |
+
content: candidate.content.parts[0].text
|
| 293 |
+
},
|
| 294 |
+
finish_reason: candidate.finishReason.toLowerCase()
|
| 295 |
+
})),
|
| 296 |
+
usage: {
|
| 297 |
+
prompt_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
|
| 298 |
+
completion_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0,
|
| 299 |
+
total_tokens: geminiResponse.usageMetadata?.totalTokenCount || 0
|
| 300 |
+
}
|
| 301 |
+
};
|
| 302 |
+
|
| 303 |
geminiKey.error_count = 0;
|
| 304 |
geminiKey.cooldown_until = null;
|
| 305 |
geminiKey.last_used_at = new Date();
|
|
|
|
| 310 |
service_key_id: req.serviceKey.key,
|
| 311 |
model_requested: model,
|
| 312 |
request_body: JSON.stringify(req.body),
|
| 313 |
+
response_body: JSON.stringify(openAIResponse),
|
| 314 |
status_code: response.status,
|
| 315 |
is_success: true,
|
| 316 |
processing_time_ms: Date.now() - startTime,
|
| 317 |
+
prompt_tokens: openAIResponse.usage.prompt_tokens,
|
| 318 |
+
completion_tokens: openAIResponse.usage.completion_tokens,
|
| 319 |
+
total_tokens: openAIResponse.usage.total_tokens,
|
| 320 |
});
|
| 321 |
|
| 322 |
+
return res.status(response.status).json(openAIResponse);
|
| 323 |
|
| 324 |
} catch (error) {
|
| 325 |
const isTimeout = error.code === 'ECONNABORTED';
|
|
|
|
| 484 |
|
| 485 |
app.get('/user/dashboard', authenticateServiceKey, async (req, res) => {
|
| 486 |
const oneDayAgo = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
|
| 487 |
+
const usageResult = await RequestLog.findOne({
|
| 488 |
attributes: [
|
| 489 |
[sequelize.fn('COUNT', sequelize.col('id')), 'requests'],
|
| 490 |
[sequelize.fn('SUM', sequelize.col('total_tokens')), 'tokens']
|
| 491 |
],
|
| 492 |
where: {
|
| 493 |
service_key_id: req.serviceKey.key,
|
| 494 |
+
createdAt: { [Op.gte]: oneDayAgo },
|
| 495 |
+
is_success: true
|
| 496 |
+
},
|
| 497 |
+
raw: true
|
| 498 |
});
|
| 499 |
|
| 500 |
+
const requestsLastDay = safeParseInt(usageResult.requests);
|
| 501 |
+
const tokensLastDay = safeParseInt(usageResult.tokens);
|
| 502 |
|
| 503 |
res.send(`
|
| 504 |
<html>
|
|
|
|
| 518 |
<li><b>Tokens per day:</b> ${req.serviceKey.tpd_limit}</li>
|
| 519 |
</ul>
|
| 520 |
<hr>
|
| 521 |
+
<h3>API Usage Example</h3>
|
| 522 |
+
<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;">curl "YOUR_SPACE_URL/v1/chat/completions" \\
|
|
|
|
| 523 |
-H "Content-Type: application/json" \\
|
| 524 |
-H "Authorization: Bearer ${req.serviceKey.key}" \\
|
| 525 |
-d '{
|
| 526 |
+
"model": "gemini-1.5-flash-latest",
|
| 527 |
"messages": [
|
| 528 |
{"role": "user", "content": "Hello!"}
|
| 529 |
]
|
|
|
|
| 547 |
],
|
| 548 |
where: {
|
| 549 |
service_key_id: req.serviceKey.key,
|
| 550 |
+
createdAt: { [Op.gte]: oneDayAgo },
|
| 551 |
+
is_success: true,
|
| 552 |
},
|
| 553 |
replacements: { oneMinuteAgo, oneDayAgo }
|
| 554 |
});
|
|
|
|
| 579 |
});
|
| 580 |
});
|
| 581 |
|
| 582 |
+
app.get(['/v1/models', '/v1beta/models'], authenticateServiceKey, async (req, res) => {
|
| 583 |
const models = Object.keys(GEMINI_DEFAULT_LIMITS).filter(model => model !== 'default').map(model => ({
|
| 584 |
id: model,
|
| 585 |
object: "model",
|
| 586 |
+
created: Math.floor(Date.now() / 1000),
|
| 587 |
owned_by: "google"
|
| 588 |
}));
|
| 589 |
|
|
|
|
| 595 |
|
| 596 |
sequelize.sync({ force: false }).then(() => {
|
| 597 |
console.log('Database initialized successfully.');
|
| 598 |
+
app.listen(PORT, '0.0.0.0', () => {
|
| 599 |
console.log(`Gemini Proxy Rotator running on port ${PORT}`);
|
|
|
|
|
|
|
| 600 |
});
|
| 601 |
}).catch(error => {
|
| 602 |
console.error('Failed to initialize database:', error);
|