From 940194d7736cdfc32af525412c9b17dec15ac856 Mon Sep 17 00:00:00 2001 From: Dustin Healy <54083382+dustinhealy@users.noreply.github.com> Date: Sun, 7 Jun 2026 17:51:48 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=93=20fix:=20throw=20on=20PII=20block?= =?UTF-8?q?=20instead=20of=20custom=20SSE=20event?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pii_blocked event needed a custom frontend handler that didn't exist. Simpler: throw a friendly Error after processStream resolves when run.getHaltReason() flags message_pii_filter_block. The existing sendCompletion catch block already pushes a ContentTypes.ERROR part into the assistant turn, so the user sees a real error message in chat with no new client code. Error body names the matched pattern labels so the user knows what was caught (e.g. "Message blocked by PII filter: Anthropic API key. Edit and retry."). Warn mode still emits the pii_matches SSE event; that one still needs a client toast handler (the frontend agent is wiring it). --- api/server/controllers/agents/client.js | 34 ++++++++++++------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 6c76a2355a..3ba9556031 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -1106,27 +1106,25 @@ class AgentClient extends BaseClient { config.signal = null; - if ( - piiFilterResult != null && - this.options.res != null && - !this.options.res.writableEnded - ) { + if (piiFilterResult != null) { const piiHaltReason = run.getHaltReason?.(); if (piiHaltReason === 'message_pii_filter_block') { - // Surface a friendly error on block mode so the frontend - // doesn't render an empty assistant bubble. The existing - // error event vocabulary is whatever the host expects; use - // a typed payload so a downstream toast/banner can render - // a real message rather than the silent halt LibreChat - // would otherwise show. - sendEvent(this.options.res, { - type: 'pii_blocked', - reason: piiHaltReason, - matches: piiFilterResult.collector.matches, - }); - } else if ( + // Throwing here routes through the existing error UX in the + // sendCompletion catch block, which pushes a ContentTypes.ERROR + // part into the assistant turn. The user sees a real error + // message in chat instead of an empty bubble + stalled + // spinner. Labels are joined so the message names what was + // matched (e.g. "Anthropic API key, GitHub token"). + const labels = piiFilterResult.collector.matches.map((m) => m.patternLabel).join(', '); + throw new Error( + `Message blocked by PII filter${labels.length > 0 ? `: ${labels}` : ''}. Edit and retry.`, + ); + } + if ( appConfig?.messagePiiFilter?.onMatch === 'warn' && - piiFilterResult.collector.matches.length > 0 + piiFilterResult.collector.matches.length > 0 && + this.options.res != null && + !this.options.res.writableEnded ) { sendEvent(this.options.res, { type: 'pii_matches',