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(`
Требуется валидный пароль администратора в параметре URL.
Пример: /admin?password=ВАШ_ПАРОЛЬ
Upload a .txt file with one Gemini API key per line.
${newKey.key}
`);
} 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(`
Owner: ${req.serviceKey.owner}
Your Service Key: ${req.serviceKey.key}
Requests: ${requestsLastDay} / ${req.serviceKey.rpd_limit}
Tokens: ${tokensLastDay} / ${req.serviceKey.tpd_limit}
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!"} ] }'`); }); 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); });