Files
wehub-resource-sync e115934061
Publish `@librechat/data-schemas` to NPM / pack (push) Failing after 8s
Publish `@librechat/client` to NPM / pack (push) Failing after 0s
GitNexus Index / index (push) Failing after 1s
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been skipped
Sync Helm Chart Tags / Sync chart tags (push) Failing after 2s
Publish `librechat-data-provider` to NPM / pack (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Failing after 1s
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Failing after 0s
Sync Helm Chart Tags / Ignore non-main push (push) Has been skipped
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Failing after 1s
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:08:12 +08:00

101 lines
3.6 KiB
JavaScript

const axios = require('axios');
const { isEnabled, getReferencedQuotes, mergeQuotedText } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { ErrorTypes } = require('librechat-data-provider');
const denyRequest = require('./denyRequest');
async function moderateText(req, res, next) {
if (!isEnabled(process.env.OPENAI_MODERATION)) {
return next();
}
try {
const { text } = req.body;
/**
* Moderate the typed text, each quoted excerpt, and the merged blockquote+text
* exactly as the model receives it. Quotes are normalized via
* `getReferencedQuotes` first (matching `BaseClient`); moderating the merged
* string also covers content split across a quote and the typed body. The
* moderation API accepts an array of inputs.
*
* `answer` covers the HITL resume payload (POST /agents/chat/resume) for an
* ask-user question — the user's free-form text — so it's moderated like a typed
* message. A tool-approval resume carries no user text and is skipped below.
*/
let safeText = '';
if (typeof text === 'string') {
safeText = text;
} else if (typeof req.body.answer === 'string') {
safeText = req.body.answer;
}
const inputs = [];
if (safeText.length > 0) {
inputs.push(safeText);
}
const quotes = getReferencedQuotes(req.body.quotes);
if (quotes != null) {
inputs.push(...quotes);
inputs.push(mergeQuotedText(safeText, quotes));
}
// A tool-approval resume can carry user-authored text in `decisions[]`: the
// `respond` substitute result, a `reject` reason, and `edit`ed tool arguments —
// moderate all of them like typed text (edited args stringified).
if (Array.isArray(req.body.decisions)) {
for (const decision of req.body.decisions) {
if (typeof decision?.responseText === 'string' && decision.responseText.length > 0) {
inputs.push(decision.responseText);
}
if (typeof decision?.reason === 'string' && decision.reason.length > 0) {
inputs.push(decision.reason);
}
if (decision?.editedArguments != null) {
try {
const edited = JSON.stringify(decision.editedArguments);
if (typeof edited === 'string' && edited.length > 0) {
inputs.push(edited);
}
} catch {
/* ignore unstringifiable edited args */
}
}
}
}
// Nothing to moderate (e.g. a tool-approval resume with no `respond` text) —
// don't post an empty/undefined `input`, which the moderation API rejects and which
// would otherwise deny the request.
if (inputs.length === 0) {
return next();
}
const input = inputs.length > 1 ? inputs : inputs[0];
const response = await axios.post(
process.env.OPENAI_MODERATION_REVERSE_PROXY || 'https://api.openai.com/v1/moderations',
{
input,
},
{
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.OPENAI_MODERATION_API_KEY}`,
},
},
);
const results = response.data.results;
const flagged = results.some((result) => result.flagged);
if (flagged) {
const type = ErrorTypes.MODERATION;
const errorMessage = { type };
return await denyRequest(req, res, errorMessage);
}
} catch (error) {
logger.error('Error in moderateText:', error);
const errorMessage = 'error in moderation check';
return await denyRequest(req, res, errorMessage);
}
next();
}
module.exports = moderateText;