FreeLLMAPI / server /dist /middleware /rateLimit.js
Nryn215's picture
Upload folder using huggingface_hub
077865a verified
Raw
History Blame Contribute Delete
2.67 kB
// Per-IP fixed-window rate limiter for the public /v1 proxy (#35, item #6).
//
// The /v1 surface authenticates with the unified API key but has no password
// login like the dashboard does, so without this an attacker who can reach the
// server could brute-force the key or flood upstream providers. This caps how
// many requests a single client IP can make per minute and returns a standard
// OpenAI-shaped 429 once the cap is exceeded.
//
// FreeLLMAPI is a single-user tool, so the default ceiling is generous. Tune it
// with PROXY_RATE_LIMIT_RPM (requests per minute per IP); set it to 0 to turn
// rate limiting off entirely.
const WINDOW_MS = 60_000;
const DEFAULT_RPM = 120;
// Bound the IP map so a flood of distinct (e.g. spoofed) source addresses can't
// grow it without limit; expired entries are pruned opportunistically.
const MAX_TRACKED_IPS = 10_000;
function parseLimit() {
const raw = process.env.PROXY_RATE_LIMIT_RPM;
if (raw === undefined || raw.trim() === '')
return DEFAULT_RPM;
const n = Number(raw);
if (!Number.isFinite(n) || n < 0)
return DEFAULT_RPM;
return Math.floor(n);
}
export function createProxyRateLimiter() {
const limit = parseLimit();
const windows = new Map();
return function proxyRateLimit(req, res, next) {
if (limit === 0) {
next();
return;
}
const now = Date.now();
const ip = req.ip ?? req.socket.remoteAddress ?? 'unknown';
let state = windows.get(ip);
if (!state || now >= state.resetAt) {
state = { count: 0, resetAt: now + WINDOW_MS };
windows.set(ip, state);
}
state.count += 1;
if (windows.size > MAX_TRACKED_IPS) {
for (const [key, value] of windows) {
if (now >= value.resetAt)
windows.delete(key);
}
}
res.setHeader('X-RateLimit-Limit', String(limit));
res.setHeader('X-RateLimit-Remaining', String(Math.max(0, limit - state.count)));
res.setHeader('X-RateLimit-Reset', String(Math.ceil(state.resetAt / 1000)));
if (state.count > limit) {
const retryAfter = Math.max(1, Math.ceil((state.resetAt - now) / 1000));
res.setHeader('Retry-After', String(retryAfter));
res.status(429).json({
error: {
message: `Rate limit exceeded: more than ${limit} requests per minute. Retry in ${retryAfter}s.`,
type: 'rate_limit_error',
},
});
return;
}
next();
};
}
//# sourceMappingURL=rateLimit.js.map