| 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 |
| }); |
|
|
| |
| 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 = { |
| |
| '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 }, |
| |
| '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 }, |
| |
| 'gemini-2.0-flash-preview-image-generation': { rpm: 10, tpm: 200000, rpd: 100 }, |
| |
| '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 }, |
| |
| 'gemini-1.5-flash-latest': { rpm: 15, tpm: 250000, rpd: 500 }, |
| 'gemini-1.5-flash-8b-latest': { rpm: 15, tpm: 250000, rpd: 500 }, |
| |
| 'gemma-3': { rpm: 30, tpm: 15000, rpd: 14400 }, |
| 'gemma-3n': { rpm: 30, tpm: 15000, rpd: 14400 }, |
| |
| 'gemini-embedding-exp-03-07': { rpm: 5, rpd: 100 }, |
|
|
| |
| |
| 'gemini-2.5-flash-preview-native-audio-dialog': { concurrentSessions: 1, tpm: 25000, rpd: 5 }, |
| |
| 'gemini-2.5-flash-exp-native-audio-thinking-dialog': { concurrentSessions: 1, tpm: 10000, rpd: 5 }, |
|
|
| |
| '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': { rpm: 15, rpd: 1500, tpm: 1000000 } |
| }; |
|
|
|
|
|
|
| |
| 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]; |
| } |
|
|
|
|
| |
| const adminAuth = (req, res, next) => { |
| const password = req.query.password; |
| if (!password || password !== ADMIN_PASSWORD) { |
| |
| 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(); |
| }; |
|
|
| |
| 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(); |
| }; |
|
|
|
|
| |
| 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); |
| }); |
|
|
| |
| |
| app.get('/admin', adminAuth, (req, res) => { |
| const adminPassword = res.locals.adminPassword; |
| 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)); |
| }); |
|
|
|
|
| |
| 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); |
| }); |
|
|
|
|