mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-11-12 09:45:47 +00:00
Some checks are pending
Backend Unit Tests / Build packages (push) Waiting to run
Backend Unit Tests / Codegraph select (push) Waiting to run
Backend Unit Tests / TypeScript type checks (push) Blocked by required conditions
Backend Unit Tests / Circular dependency checks (push) Waiting to run
Backend Unit Tests / Tests: api (shard 1/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 2/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: api (shard 3/3) (push) Blocked by required conditions
Backend Unit Tests / Tests: data-provider (push) Blocked by required conditions
Backend Unit Tests / Tests: data-schemas (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 1/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 2/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 3/4) (push) Blocked by required conditions
Backend Unit Tests / Tests: @librechat/api (shard 4/4) (push) Blocked by required conditions
Codegraph E2E Votes / vote (full suite) (push) Waiting to run
Docker Dev Branch Images Build / publish (push) Waiting to run
Frontend Unit Tests / Client build recovery regression (push) Waiting to run
Frontend Unit Tests / Codegraph select (push) Waiting to run
Frontend Unit Tests / Build packages (push) Waiting to run
Frontend Unit Tests / TypeScript type checks (client) (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/chat (push) Blocked by required conditions
Frontend Unit Tests / Tests: @librechat/client (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 1/2) (push) Blocked by required conditions
Frontend Unit Tests / Tests: Ubuntu (shard 2/2) (push) Blocked by required conditions
Frontend Unit Tests / Vite build verification (push) Blocked by required conditions
* fix: Bound Outbound Calls Made Inside a Request RAG queries for file search and message context, OCR provider calls, action HTTP calls, the moderation check, the stored-image fetch for base64 providers, and Flux result polling had no timeout. A stalled upstream hung the chat turn, the upload or the tool call indefinitely, and Flux kept polling forever on statuses that never become Ready. Each now runs under a budget from a new librechat.yaml timeouts block with finite defaults (getOutboundTimeoutMs in @librechat/api). Action calls also receive the run's abort signal, so stopping a run closes the socket. * test: Cover a Stalled RAG Query in the Mock Harness * fix: Share One Flux Budget Across Submit and Polls, Bound the Vertex Token Exchange * refactor: Poll Flux Results Through pollUntilSettled * refactor: Leave Flux Polling for a Separate Migration * fix: Cap Configured Timeouts at the Node Timer Limit * fix: Bound the Action Token Refresh and the Whole Image Download * fix: Cap Every Outbound Budget by Wall-Clock Time Axios's timeout only fires on an idle socket, so a RAG, OCR, moderation, action or token response that keeps trickling bytes never ended. outboundRequestBudget pairs each timeout with an AbortSignal deadline, and action calls combine it with the run's own signal. * fix: Name the Budget That Ran Out and Give Actions Five Minutes The actions default rises to five minutes, since action endpoints are arbitrary third-party APIs and the old minute could cut off legitimately slow ones after an upgrade. A budget overrun now fails with an error naming the timeouts key and its value, whether it was the idle timeout or the wall-clock deadline, and logAxiosError logs it as a warning so operators know what to raise. A deadline cancellation is no longer read as a user stop, so an action that runs out of time reports a timeout instead of looking like a stopped run. * fix: Hold Outbound Deadlines Weakly So Finished Requests Leave No Timers * fix: Check the Budget Before Reading an Error Message * fix: Report a Wrapped Deadline as a Failure, Not a Stop * fix: Bound the OAuth Code Exchange and Keep the OCR Budget Hint When Protected * chore: Sort Imports in axios.ts After Rebase
121 lines
4.1 KiB
JavaScript
121 lines
4.1 KiB
JavaScript
const axios = require('axios');
|
|
const {
|
|
isEnabled,
|
|
getReferencedQuotes,
|
|
mergeQuotedText,
|
|
getBoundedAskUserAnswerValues,
|
|
serializeAskUserAnswerVariants,
|
|
getOutboundTimeoutMs,
|
|
outboundRequestBudget,
|
|
logAxiosError,
|
|
} = require('@librechat/api');
|
|
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);
|
|
}
|
|
if (
|
|
req.body.answers != null &&
|
|
typeof req.body.answers === 'object' &&
|
|
!Array.isArray(req.body.answers)
|
|
) {
|
|
for (const answer of getBoundedAskUserAnswerValues(req.body.answers)) {
|
|
if (answer.length > 0) {
|
|
inputs.push(answer);
|
|
}
|
|
}
|
|
inputs.push(...serializeAskUserAnswerVariants(req.body.answers));
|
|
}
|
|
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}`,
|
|
},
|
|
...outboundRequestBudget(getOutboundTimeoutMs(req.config, 'moderationMs'), 'moderationMs'),
|
|
},
|
|
);
|
|
|
|
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) {
|
|
logAxiosError({ message: 'Error in moderateText:', error });
|
|
const errorMessage = 'error in moderation check';
|
|
return await denyRequest(req, res, errorMessage);
|
|
}
|
|
next();
|
|
}
|
|
|
|
module.exports = moderateText;
|