gp / server.js
opex792's picture
Update server.js
244353a verified
import express from 'express';
import { Sequelize, DataTypes, Op } from 'sequelize';
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import fileUpload from 'express-fileupload';
import path from 'path';
import { fileURLToPath } from 'url';
import fs from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PORT = process.env.PORT || 7860;
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
if (!ADMIN_PASSWORD) {
console.error("CRITICAL ERROR: The ADMIN_PASSWORD environment variable is not set. Set it in your Hugging Face Space secrets.");
process.exit(1);
}
const dataDir = path.join(__dirname, 'data');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
const DB_PATH = path.join(dataDir, 'database.sqlite');
const KEY_DEACTIVATION_THRESHOLD = 5;
const KEY_COOLDOWN_SECONDS = 60;
const app = express();
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(fileUpload());
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: DB_PATH,
logging: false
});
// ... (Модели Sequelize остаются без изменений: GeminiKey, RequestLog, ServiceKey)
const GeminiKey = sequelize.define('GeminiKey', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
key: { type: DataTypes.STRING, allowNull: false, unique: true },
is_active: { type: DataTypes.BOOLEAN, defaultValue: true },
last_used_at: { type: DataTypes.DATE },
error_count: { type: DataTypes.INTEGER, defaultValue: 0 },
last_error: { type: DataTypes.STRING },
cooldown_until: { type: DataTypes.DATE, defaultValue: null }
}, { timestamps: true });
const RequestLog = sequelize.define('RequestLog', {
id: { type: DataTypes.UUID, primaryKey: true, defaultValue: DataTypes.UUIDV4 },
gemini_key_id: { type: DataTypes.INTEGER, references: { model: GeminiKey, key: 'id' } },
service_key_id: { type: DataTypes.STRING },
model_requested: { type: DataTypes.STRING },
request_body: { type: DataTypes.TEXT },
response_body: { type: DataTypes.TEXT },
status_code: { type: DataTypes.INTEGER },
is_success: { type: DataTypes.BOOLEAN },
error_message: { type: DataTypes.TEXT },
processing_time_ms: { type: DataTypes.INTEGER },
prompt_tokens: { type: DataTypes.INTEGER, defaultValue: 0 },
completion_tokens: { type: DataTypes.INTEGER, defaultValue: 0 },
total_tokens: { type: DataTypes.INTEGER, defaultValue: 0 }
}, { timestamps: true });
const ServiceKey = sequelize.define('ServiceKey', {
key: { type: DataTypes.STRING, primaryKey: true, defaultValue: () => `sk-gemini-proxy-${uuidv4()}` },
owner: { type: DataTypes.STRING, allowNull: false },
is_active: { type: DataTypes.BOOLEAN, defaultValue: true },
rpm_limit: { type: DataTypes.INTEGER, defaultValue: 15 },
rpd_limit: { type: DataTypes.INTEGER, defaultValue: 1500 },
tpm_limit: { type: DataTypes.INTEGER, defaultValue: 1000000 },
tpd_limit: { type: DataTypes.INTEGER, defaultValue: 2000000 },
}, { timestamps: true });
GeminiKey.hasMany(RequestLog, { foreignKey: 'gemini_key_id' });
RequestLog.belongsTo(GeminiKey, { foreignKey: 'gemini_key_id' });
const GEMINI_DEFAULT_LIMITS = {
// Models from the "Текущие ограничения по ставкам" (Current rate limits) table
'gemini-2.5-flash-preview-05-20': { rpm: 10, tpm: 250000, rpd: 500 },
'gemini-2.5-flash-preview-tts': { rpm: 3, tpm: 10000, rpd: 15 },
// Mapped from "Gemini 2.5 Pro Экспериментальный 03-25"
'gemini-2.5-pro-preview-03-25': { rpm: 5, tpm: 250000, tpd: 1000000, rpd: 25 },
'gemini-2.0-flash': { rpm: 15, tpm: 1000000, rpd: 1500 },
// Mapped from "Генерация изображений предварительного просмотра Flash Gemini 2.0"
'gemini-2.0-flash-preview-image-generation': { rpm: 10, tpm: 200000, rpd: 100 },
// Mapped from "Gemini 2.0 Flash Экспериментальный"
'gemini-2.0-flash-exp': { rpm: 10, tpm: 250000, rpd: 1000 },
'gemini-2.0-flash-lite': { rpm: 30, tpm: 1000000, rpd: 1500 },
'gemini-1.5-flash': { rpm: 15, tpm: 250000, rpd: 500 },
'gemini-1.5-flash-8b': { rpm: 15, tpm: 250000, rpd: 500 },
// "latest" aliases pointing to the same limits
'gemini-1.5-flash-latest': { rpm: 15, tpm: 250000, rpd: 500 },
'gemini-1.5-flash-8b-latest': { rpm: 15, tpm: 250000, rpd: 500 },
// Gemma models
'gemma-3': { rpm: 30, tpm: 15000, rpd: 14400 },
'gemma-3n': { rpm: 30, tpm: 15000, rpd: 14400 },
// Embedding model
'gemini-embedding-exp-03-07': { rpm: 5, rpd: 100 }, // TPM is not specified in the docs
// Models from "Ограничения скорости АРІ в реальном времени" (Real-time API rate limits) table
// Mapped from "Gemini 2.5 Flash Preview Собственный аудиодиалог"
'gemini-2.5-flash-preview-native-audio-dialog': { concurrentSessions: 1, tpm: 25000, rpd: 5 },
// Mapped from "Gemini 2.5 Flash Experimental Native Audio Thinking Dialog"
'gemini-2.5-flash-exp-native-audio-thinking-dialog': { concurrentSessions: 1, tpm: 10000, rpd: 5 },
// Models with no specified limits in the provided "Free tier" table
'gemini-1.5-pro': null,
'gemini-1.5-pro-latest': null,
'gemini-2.5-pro': null,
'gemini-2.5-pro-preview-06-05': null,
'imagen-3.0-generate-002': null,
'veo-2.0-generate-001': null,
// Default as a fallback, based on a common powerful model like gemini-2.0-flash
'default': { rpm: 15, rpd: 1500, tpm: 1000000 }
};
// ... (Функции getModelLimits, safeParseInt, selectBestGeminiKey остаются без изменений)
function getModelLimits(model) {
const foundModel = Object.keys(GEMINI_DEFAULT_LIMITS).find(k => model.startsWith(k));
return GEMINI_DEFAULT_LIMITS[foundModel] || GEMINI_DEFAULT_LIMITS['default'];
}
function safeParseInt(value, defaultValue = 0) {
const parsed = Number(value);
return isNaN(parsed) ? defaultValue : Math.floor(parsed);
}
async function selectBestGeminiKey(model, attemptedKeys = new Set()) {
const now = new Date();
const oneMinuteAgo = new Date(now.getTime() - 60 * 1000);
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const activeKeys = await GeminiKey.findAll({
where: {
is_active: true,
id: { [Op.notIn]: [...attemptedKeys] },
[Op.or]: [
{ cooldown_until: null },
{ cooldown_until: { [Op.lt]: now } }
]
},
order: [['last_used_at', 'ASC']]
});
if (activeKeys.length === 0) return null;
const keyUsageStats = await RequestLog.findAll({
attributes: [
'gemini_key_id',
[sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneMinuteAgo THEN 1 ELSE 0 END`)), 'requestsLastMinute'],
[sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneDayAgo THEN 1 ELSE 0 END`)), 'requestsLastDay'],
[sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneMinuteAgo THEN COALESCE(total_tokens, 0) ELSE 0 END`)), 'tokensLastMinute'],
[sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneDayAgo THEN COALESCE(total_tokens, 0) ELSE 0 END`)), 'tokensLastDay'],
],
where: {
is_success: true,
gemini_key_id: { [Op.in]: activeKeys.map(k => k.id) },
createdAt: { [Op.gte]: oneDayAgo }
},
group: ['gemini_key_id'],
replacements: { oneMinuteAgo, oneDayAgo }
});
const usageMap = keyUsageStats.reduce((acc, usage) => {
const data = usage.get();
acc[data.gemini_key_id] = {
rpm: safeParseInt(data.requestsLastMinute),
rpd: safeParseInt(data.requestsLastDay),
tpm: safeParseInt(data.tokensLastMinute),
tpd: safeParseInt(data.tokensLastDay),
};
return acc;
}, {});
let bestKey = null;
let minUsagePercentage = Infinity;
for (const key of activeKeys) {
const limits = getModelLimits(model);
const usage = usageMap[key.id] || { rpm: 0, rpd: 0, tpm: 0, tpd: 0 };
const rpmUsage = (usage.rpm / limits.rpm) * 100;
const rpdUsage = (usage.rpd / limits.rpd) * 100;
const tpmUsage = (usage.tpm / limits.tpm) * 100;
const tpdUsage = (usage.tpd / limits.tpd) * 100;
const maxUsageForThisKey = Math.max(rpmUsage, rpdUsage, tpmUsage, tpdUsage);
if (maxUsageForThisKey < minUsagePercentage) {
minUsagePercentage = maxUsageForThisKey;
bestKey = key;
}
}
return bestKey || activeKeys[0];
}
// --- ИЗМЕНЕНИЕ: Middleware для авторизации админа через query-параметр ---
const adminAuth = (req, res, next) => {
const password = req.query.password;
if (!password || password !== ADMIN_PASSWORD) {
// Ответ в HTML для удобства, если заходят через браузер
return res.status(401).send(`
<div style="font-family: sans-serif; text-align: center; padding-top: 50px;">
<h1>401 Unauthorized</h1>
<p>Требуется валидный пароль администратора в параметре URL.</p>
<p>Пример: <code>/admin?password=ВАШ_ПАРОЛЬ</code></p>
</div>
`);
}
// Сохраняем пароль для использования в шаблонах и ссылках
res.locals.adminPassword = password;
next();
};
// ... (authenticateServiceKey middleware остается без изменений)
const authenticateServiceKey = async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authorization header is missing or invalid. Expected: Bearer YOUR_SERVICE_KEY' });
}
const key = authHeader.split(' ')[1];
const serviceKey = await ServiceKey.findByPk(key);
if (!serviceKey || !serviceKey.is_active) {
return res.status(403).json({ error: 'Invalid or inactive service key.' });
}
req.serviceKey = serviceKey;
const now = new Date();
const oneMinuteAgo = new Date(now.getTime() - 60 * 1000);
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const usageResult = await RequestLog.findOne({
attributes: [
[sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneMinuteAgo THEN 1 ELSE 0 END`)), 'requestsLastMinute'],
[sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneDayAgo THEN 1 ELSE 0 END`)), 'requestsLastDay'],
[sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneMinuteAgo THEN COALESCE(total_tokens, 0) ELSE 0 END`)), 'tokensLastMinute'],
[sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneDayAgo THEN COALESCE(total_tokens, 0) ELSE 0 END`)), 'tokensLastDay']
],
where: {
service_key_id: key,
createdAt: { [Op.gte]: oneDayAgo }
},
replacements: { oneMinuteAgo, oneDayAgo }
});
const usage = {
requestsLastMinute: safeParseInt(usageResult?.get('requestsLastMinute')),
requestsLastDay: safeParseInt(usageResult?.get('requestsLastDay')),
tokensLastMinute: safeParseInt(usageResult?.get('tokensLastMinute')),
tokensLastDay: safeParseInt(usageResult?.get('tokensLastDay')),
};
if (usage.requestsLastMinute >= serviceKey.rpm_limit) {
return res.status(429).json({ error: `Rate limit exceeded. Your RPM limit is ${serviceKey.rpm_limit}.` });
}
if (usage.tokensLastMinute >= serviceKey.tpm_limit) {
return res.status(429).json({ error: `Token limit exceeded. Your TPM limit is ${serviceKey.tpm_limit}.` });
}
if (usage.requestsLastDay >= serviceKey.rpd_limit) {
return res.status(429).json({ error: `Daily request limit exceeded. Your RPD limit is ${serviceKey.rpd_limit}.` });
}
if (usage.tokensLastDay >= serviceKey.tpd_limit) {
return res.status(429).json({ error: `Daily token limit exceeded. Your TPD limit is ${serviceKey.tpd_limit}.` });
}
next();
};
// ... (Основной эндпоинт /v1/chat/completions остается без изменений)
app.post(['/v1/chat/completions', '/v1beta/chat/completions'], authenticateServiceKey, async (req, res) => {
if (!req.body || typeof req.body !== 'object') {
return res.status(400).json({ error: 'Request body must be a valid JSON object.' });
}
const startTime = Date.now();
const model = req.body.model;
if (!model) {
return res.status(400).json({ error: 'The "model" field is required in the request body.' });
}
let geminiKey = null;
const maxAttempts = await GeminiKey.count({ where: { is_active: true } });
const attemptedKeys = new Set();
let lastError = null;
let lastStatusCode = 500;
while (attemptedKeys.size < maxAttempts) {
geminiKey = await selectBestGeminiKey(model, attemptedKeys);
if (!geminiKey) break;
attemptedKeys.add(geminiKey.id);
try {
const geminiApiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
const geminiRequestBody = {
contents: req.body.messages.map(msg => ({
role: msg.role === 'assistant' ? 'model' : msg.role,
parts: [{ text: msg.content }]
})),
generationConfig: {
temperature: req.body.temperature,
topP: req.body.top_p,
maxOutputTokens: req.body.max_tokens,
}
};
const response = await axios.post(geminiApiUrl, geminiRequestBody, {
headers: {
'Content-Type': 'application/json',
'x-goog-api-key': geminiKey.key
},
timeout: 45000
});
const geminiResponse = response.data;
const openAIResponse = {
id: `chatcmpl-${uuidv4()}`,
object: 'chat.completion',
created: Math.floor(Date.now() / 1000),
model: model,
choices: geminiResponse.candidates.map((candidate, index) => ({
index: index,
message: {
role: 'assistant',
content: candidate.content.parts[0].text
},
finish_reason: candidate.finishReason.toLowerCase()
})),
usage: {
prompt_tokens: geminiResponse.usageMetadata?.promptTokenCount || 0,
completion_tokens: geminiResponse.usageMetadata?.candidatesTokenCount || 0,
total_tokens: geminiResponse.usageMetadata?.totalTokenCount || 0
}
};
geminiKey.error_count = 0;
geminiKey.cooldown_until = null;
geminiKey.last_used_at = new Date();
await geminiKey.save();
await RequestLog.create({
gemini_key_id: geminiKey.id,
service_key_id: req.serviceKey.key,
model_requested: model,
request_body: JSON.stringify(req.body),
response_body: JSON.stringify(openAIResponse),
status_code: response.status,
is_success: true,
processing_time_ms: Date.now() - startTime,
prompt_tokens: openAIResponse.usage.prompt_tokens,
completion_tokens: openAIResponse.usage.completion_tokens,
total_tokens: openAIResponse.usage.total_tokens,
});
return res.status(response.status).json(openAIResponse);
} catch (error) {
const isTimeout = error.code === 'ECONNABORTED';
const errorMessage = isTimeout ? 'Request timed out' : (error.response ? JSON.stringify(error.response.data) : error.message);
const statusCode = isTimeout ? 504 : (error.response ? error.response.status : 500);
lastError = errorMessage;
lastStatusCode = statusCode;
await RequestLog.create({
gemini_key_id: geminiKey ? geminiKey.id : null,
service_key_id: req.serviceKey.key,
model_requested: model,
request_body: JSON.stringify(req.body),
status_code: statusCode,
is_success: false,
error_message: errorMessage,
processing_time_ms: Date.now() - startTime
});
if (geminiKey) {
geminiKey.error_count += 1;
geminiKey.last_error = errorMessage.substring(0, 255);
if (statusCode === 429) {
geminiKey.cooldown_until = new Date(Date.now() + KEY_COOLDOWN_SECONDS * 1000);
} else if (statusCode === 400 || statusCode === 403 || geminiKey.error_count >= KEY_DEACTIVATION_THRESHOLD) {
geminiKey.is_active = false;
}
await geminiKey.save();
}
}
}
let errorDetails;
try {
errorDetails = lastError ? JSON.parse(lastError) : { message: 'No error details available.' };
} catch (e) {
errorDetails = { raw_error: lastError };
}
const finalError = {
error: "Failed to process request. All available Gemini keys resulted in errors.",
details: errorDetails
};
if (lastStatusCode === 429) {
finalError.error = "Service is overloaded. All available Gemini keys have reached their rate limits. Please try again later.";
}
return res.status(lastStatusCode).json(finalError);
});
// --- ИЗМЕНЕНИЕ: Добавляем middleware adminAuth ко всем админским роутам ---
// --- ИЗМЕНЕНИЕ: Обновляем HTML админки для использования query-параметра ---
app.get('/admin', adminAuth, (req, res) => {
const adminPassword = res.locals.adminPassword; // Получаем пароль из middleware
res.send(`
<html>
<head><title>Admin Panel</title><meta name="viewport" content="width=device-width, initial-scale=1"></head>
<body style="font-family: sans-serif; padding: 20px; max-width: 800px; margin: auto;">
<h1>Gemini Proxy Admin Panel</h1>
<h2>Upload Gemini Keys</h2>
<p>Upload a .txt file with one Gemini API key per line.</p>
<form action="/admin/upload-keys?password=${adminPassword}" method="post" enctype="multipart/form-data">
<input type="file" name="keysFile" accept=".txt" required>
<button type="submit">Upload</button>
</form>
<hr>
<h2>Manage Service Keys</h2>
<form action="/admin/service-keys?password=${adminPassword}" method="post" target="creation-result">
<input type="text" name="owner" placeholder="Owner (e.g., user@example.com)" required style="width: 250px; padding: 5px;">
<button type="submit">Create New Service Key</button>
</form>
<iframe name="creation-result" style="width: 100%; height: 50px; border: 1px solid #ccc; margin-top: 10px;"></iframe>
<hr>
<h2>Stats & Data</h2>
<ul>
<li><a href="/admin/stats?password=${adminPassword}" target="_blank">View Stats (JSON)</a></li>
<li><a href="/admin/download/geminikeys?password=${adminPassword}">Download GeminiKeys Table (JSON)</a></li>
<li><a href="/admin/download/requestlogs?password=${adminPassword}">Download RequestLogs Table (JSON)</a></li>
<li><a href="/admin/download/servicekeys?password=${adminPassword}">Download ServiceKeys Table (JSON)</a></li>
</ul>
</body>
</html>
`);
});
app.post('/admin/upload-keys', adminAuth, async (req, res) => {
if (!req.files || !req.files.keysFile) {
return res.status(400).send('No files were uploaded.');
}
const keysFile = req.files.keysFile;
const keys = keysFile.data.toString('utf8').split(/\r?\n/).filter(key => key.trim() !== '');
let newKeysCount = 0;
for (const key of keys) {
const trimmedKey = key.trim();
if (!trimmedKey) continue;
const [_, created] = await GeminiKey.findOrCreate({
where: { key: trimmedKey },
defaults: { key: trimmedKey }
});
if (created) newKeysCount++;
}
res.send(`File processed successfully. Added ${newKeysCount} new unique keys.`);
});
app.post('/admin/service-keys', adminAuth, async (req, res) => {
const { owner } = req.body;
if (!owner) return res.status(400).send('Owner is required.');
try {
const newKey = await ServiceKey.create({ owner });
res.status(201).send(`<b>Key created successfully!</b><br><code>${newKey.key}</code>`);
} catch (error) {
res.status(500).json({ message: "Failed to create service key", error: error.message });
}
});
app.get('/admin/stats', adminAuth, async (req, res) => {
const totalGeminiKeys = await GeminiKey.count();
const activeGeminiKeys = await GeminiKey.count({ where: { is_active: true } });
const totalRequests = await RequestLog.count();
const successfulRequests = await RequestLog.count({ where: { is_success: true } });
const totalServiceKeys = await ServiceKey.count();
res.json({
gemini_keys: {
total: totalGeminiKeys,
active: activeGeminiKeys,
inactive: totalGeminiKeys - activeGeminiKeys,
},
requests: {
total: totalRequests,
successful: successfulRequests,
failed: totalRequests - successfulRequests,
success_rate: totalRequests > 0 ? ((successfulRequests / totalRequests) * 100).toFixed(2) + '%' : 'N/A',
},
service_keys_total: totalServiceKeys,
});
});
app.get('/admin/download/:table', adminAuth, async (req, res) => {
const { table } = req.params;
const modelMap = {
'geminikeys': GeminiKey,
'requestlogs': RequestLog,
'servicekeys': ServiceKey
};
const model = modelMap[table.toLowerCase()];
if (!model) return res.status(404).send('Table not found');
const data = await model.findAll({ order: [['createdAt', 'DESC']] });
res.header('Content-Type', 'application/json');
res.header('Content-Disposition', `attachment; filename=${table}.json`);
res.send(JSON.stringify(data, null, 2));
});
// ... (Роуты для пользователей /user/dashboard, /user/stats, /v1/models остаются без изменений)
app.get('/user/dashboard', authenticateServiceKey, async (req, res) => {
const oneDayAgo = new Date(new Date().getTime() - 24 * 60 * 60 * 1000);
const usageResult = await RequestLog.findOne({
attributes: [
[sequelize.fn('COUNT', sequelize.col('id')), 'requests'],
[sequelize.fn('SUM', sequelize.col('total_tokens')), 'tokens']
],
where: {
service_key_id: req.serviceKey.key,
createdAt: { [Op.gte]: oneDayAgo },
is_success: true
},
raw: true
});
const requestsLastDay = safeParseInt(usageResult.requests);
const tokensLastDay = safeParseInt(usageResult.tokens);
res.send(`
<html>
<head><title>User Dashboard</title><meta name="viewport" content="width=device-width, initial-scale=1"></head>
<body style="font-family: sans-serif; padding: 20px; max-width: 800px; margin: auto;">
<h1>User Dashboard</h1>
<p><b>Owner:</b> ${req.serviceKey.owner}</p>
<p><b>Your Service Key:</b> <code style="background: #eee; padding: 2px 5px; border-radius: 4px;">${req.serviceKey.key}</code></p>
<h3>Your Daily Usage & Limits (last 24h)</h3>
<p><b>Requests:</b> ${requestsLastDay} / ${req.serviceKey.rpd_limit}</p>
<p><b>Tokens:</b> ${tokensLastDay} / ${req.serviceKey.tpd_limit}</p>
<h3>Your Rate Limits</h3>
<ul>
<li><b>Requests per minute:</b> ${req.serviceKey.rpm_limit}</li>
<li><b>Requests per day:</b> ${req.serviceKey.rpd_limit}</li>
<li><b>Tokens per minute:</b> ${req.serviceKey.tpm_limit}</li>
<li><b>Tokens per day:</b> ${req.serviceKey.tpd_limit}</li>
</ul>
<hr>
<h3>API Usage Example</h3>
<pre style="background: #f5f5f5; padding: 10px; border-radius: 4px; overflow-x: auto;">curl "YOUR_SPACE_URL/v1/chat/completions" \\
-H "Content-Type: application/json" \\
-H "Authorization: Bearer ${req.serviceKey.key}" \\
-d '{
"model": "gemini-1.5-flash-latest",
"messages": [
{"role": "user", "content": "Hello!"}
]
}'</pre>
</body>
</html>
`);
});
app.get('/user/stats', authenticateServiceKey, async (req, res) => {
const now = new Date();
const oneMinuteAgo = new Date(now.getTime() - 60 * 1000);
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const usageResult = await RequestLog.findOne({
attributes: [
[sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneMinuteAgo THEN 1 ELSE 0 END`)), 'requestsLastMinute'],
[sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneDayAgo THEN 1 ELSE 0 END`)), 'requestsLastDay'],
[sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneMinuteAgo THEN COALESCE(total_tokens, 0) ELSE 0 END`)), 'tokensLastMinute'],
[sequelize.fn('SUM', sequelize.literal(`CASE WHEN "createdAt" >= :oneDayAgo THEN COALESCE(total_tokens, 0) ELSE 0 END`)), 'tokensLastDay']
],
where: {
service_key_id: req.serviceKey.key,
createdAt: { [Op.gte]: oneDayAgo },
is_success: true,
},
replacements: { oneMinuteAgo, oneDayAgo }
});
const usage = {
requestsLastMinute: safeParseInt(usageResult?.get('requestsLastMinute')),
requestsLastDay: safeParseInt(usageResult?.get('requestsLastDay')),
tokensLastMinute: safeParseInt(usageResult?.get('tokensLastMinute')),
tokensLastDay: safeParseInt(usageResult?.get('tokensLastDay')),
};
res.json({
service_key: req.serviceKey.key,
owner: req.serviceKey.owner,
limits: {
rpm: req.serviceKey.rpm_limit,
rpd: req.serviceKey.rpd_limit,
tpm: req.serviceKey.tpm_limit,
tpd: req.serviceKey.tpd_limit
},
current_usage: usage,
usage_percentages: {
rpm: ((usage.requestsLastMinute / req.serviceKey.rpm_limit) * 100).toFixed(2) + '%',
rpd: ((usage.requestsLastDay / req.serviceKey.rpd_limit) * 100).toFixed(2) + '%',
tpm: ((usage.tokensLastMinute / req.serviceKey.tpm_limit) * 100).toFixed(2) + '%',
tpd: ((usage.tokensLastDay / req.serviceKey.tpd_limit) * 100).toFixed(2) + '%'
}
});
});
app.get(['/v1/models', '/v1beta/models'], authenticateServiceKey, async (req, res) => {
const models = Object.keys(GEMINI_DEFAULT_LIMITS).filter(model => model !== 'default').map(model => ({
id: model,
object: "model",
created: Math.floor(Date.now() / 1000),
owned_by: "google"
}));
res.json({
object: "list",
data: models
});
});
sequelize.sync({ force: false }).then(() => {
console.log('Database initialized successfully.');
app.listen(PORT, '0.0.0.0', () => {
console.log(`Gemini Proxy Rotator running on port ${PORT}`);
});
}).catch(error => {
console.error('Failed to initialize database:', error);
process.exit(1);
});