🎚️ feat: Configure Agent Event Runtime in YAML (#15128)

This commit is contained in:
Danny Avila 2026-08-23 02:37:33 -04:00 committed by GitHub
parent dd146ff74d
commit 8969ee4b18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 233 additions and 65 deletions

View file

@ -9,8 +9,6 @@ const {
MESSAGE_IP_WINDOW = 1,
MESSAGE_USER_MAX = 40,
MESSAGE_USER_WINDOW = 1,
AGENT_EVENT_USER_MAX = 40,
AGENT_EVENT_USER_WINDOW = 1,
MESSAGE_VIOLATION_SCORE: score,
} = process.env;
@ -22,10 +20,6 @@ const userWindowMs = MESSAGE_USER_WINDOW * 60 * 1000;
const userMax = MESSAGE_USER_MAX;
const userWindowInMinutes = userWindowMs / 60000;
const agentEventUserWindowMs = AGENT_EVENT_USER_WINDOW * 60 * 1000;
const agentEventUserMax = AGENT_EVENT_USER_MAX;
const agentEventUserWindowInMinutes = agentEventUserWindowMs / 60000;
/**
* Creates either an IP/User message request rate limiter for excessive requests
* that properly logs and denies the violation.
@ -85,23 +79,31 @@ const messageUserLimiter = rateLimit(userLimiterOptions);
* consumes the normal message-user bucket when it executes the delivery, so
* sharing that limiter here would charge every event twice.
*/
const agentEventUserLimiter = rateLimit({
windowMs: agentEventUserWindowMs,
max: agentEventUserMax,
handler: async (req, res) => {
const type = ViolationTypes.MESSAGE_LIMIT;
const errorMessage = {
type,
max: agentEventUserMax,
limiter: 'agent_event_principal',
windowInMinutes: agentEventUserWindowInMinutes,
};
await logViolation(req, res, type, errorMessage, score);
return await denyRequest(req, res, errorMessage);
},
keyGenerator: (req) => String(req.apiKeyId ?? req.user?.id),
store: limiterCache('agent_event_user_limiter'),
});
let configuredAgentEventUserLimiter;
const agentEventUserLimiter = (req, res, next) => {
if (configuredAgentEventUserLimiter == null) {
const max = Number(process.env.AGENT_EVENT_USER_MAX ?? 40);
const windowInMinutes = Number(process.env.AGENT_EVENT_USER_WINDOW ?? 1);
configuredAgentEventUserLimiter = rateLimit({
windowMs: windowInMinutes * 60 * 1000,
max,
handler: async (limitedReq, limitedRes) => {
const type = ViolationTypes.MESSAGE_LIMIT;
const errorMessage = {
type,
max,
limiter: 'agent_event_principal',
windowInMinutes,
};
await logViolation(limitedReq, limitedRes, type, errorMessage, score);
return await denyRequest(limitedReq, limitedRes, errorMessage);
},
keyGenerator: (limitedReq) => String(limitedReq.apiKeyId ?? limitedReq.user?.id),
store: limiterCache('agent_event_user_limiter'),
});
}
return configuredAgentEventUserLimiter(req, res, next);
};
module.exports = {
agentEventUserLimiter,

View file

@ -0,0 +1,26 @@
const mockLimiter = jest.fn((_req, _res, next) => next());
const mockRateLimit = jest.fn(() => mockLimiter);
jest.mock('express-rate-limit', () => mockRateLimit);
jest.mock('@librechat/api', () => ({
limiterCache: jest.fn(() => ({})),
removePorts: jest.fn(),
}));
jest.mock('~/server/middleware/denyRequest', () => jest.fn());
jest.mock('~/cache', () => ({ logViolation: jest.fn() }));
describe('agent event rate limiter', () => {
it('reads YAML-projected limits lazily after startup configuration', () => {
process.env.AGENT_EVENT_USER_MAX = '80';
process.env.AGENT_EVENT_USER_WINDOW = '2';
const { agentEventUserLimiter } = require('./messageLimiters');
const next = jest.fn();
agentEventUserLimiter({ apiKeyId: 'key-1' }, {}, next);
expect(mockRateLimit).toHaveBeenLastCalledWith(
expect.objectContaining({ max: 80, windowMs: 120_000 }),
);
expect(mockLimiter).toHaveBeenCalledWith({ apiKeyId: 'key-1' }, {}, next);
});
});