|
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); |
|
}); |
|
|
|
|