From 524fc5bae4fb41deb87bd4851578b804d1a1d415 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 13 Nov 2025 08:34:55 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20feat:=20Add=20Model=20R?= =?UTF-8?q?efusal=20Error=20Handling=20(Anthropic)=20(#10478)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add error handling for model refusal and update translations * refactor: error handling in AgentClient to improve logging and cleanup process * refactor: Update error message for response refusal to improve clarity --- api/server/controllers/agents/callbacks.js | 31 +++++++++++-- api/server/controllers/agents/client.js | 46 +++++++++---------- .../src/components/Messages/Content/Error.tsx | 1 + client/src/locales/en/translation.json | 1 + packages/data-provider/src/config.ts | 4 ++ 5 files changed, 54 insertions(+), 29 deletions(-) diff --git a/api/server/controllers/agents/callbacks.js b/api/server/controllers/agents/callbacks.js index 82c9c72168..65f5501416 100644 --- a/api/server/controllers/agents/callbacks.js +++ b/api/server/controllers/agents/callbacks.js @@ -1,7 +1,7 @@ const { nanoid } = require('nanoid'); const { sendEvent } = require('@librechat/api'); const { logger } = require('@librechat/data-schemas'); -const { Tools, StepTypes, FileContext } = require('librechat-data-provider'); +const { Tools, StepTypes, FileContext, ErrorTypes } = require('librechat-data-provider'); const { EnvVar, Providers, @@ -27,6 +27,13 @@ class ModelEndHandler { this.collectedUsage = collectedUsage; } + finalize(errorMessage) { + if (!errorMessage) { + return; + } + throw new Error(errorMessage); + } + /** * @param {string} event * @param {ModelEndData | undefined} data @@ -40,10 +47,25 @@ class ModelEndHandler { return; } + /** @type {string | undefined} */ + let errorMessage; try { const agentContext = graph.getAgentContext(metadata); const isGoogle = agentContext.provider === Providers.GOOGLE; const streamingDisabled = !!agentContext.clientOptions?.disableStreaming; + if (data?.output?.additional_kwargs?.stop_reason === 'refusal') { + const info = { ...data.output.additional_kwargs }; + errorMessage = JSON.stringify({ + type: ErrorTypes.REFUSAL, + info, + }); + logger.debug(`[ModelEndHandler] Model refused to respond`, { + ...info, + userId: metadata.user_id, + messageId: metadata.run_id, + conversationId: metadata.thread_id, + }); + } const toolCalls = data?.output?.tool_calls; let hasUnprocessedToolCalls = false; @@ -62,7 +84,7 @@ class ModelEndHandler { const usage = data?.output?.usage_metadata; if (!usage) { - return; + return this.finalize(errorMessage); } const modelName = metadata?.ls_model_name || agentContext.clientOptions?.model; if (modelName) { @@ -71,10 +93,10 @@ class ModelEndHandler { this.collectedUsage.push(usage); if (!streamingDisabled) { - return; + return this.finalize(errorMessage); } if (!data.output.content) { - return; + return this.finalize(errorMessage); } const stepKey = graph.getStepKey(metadata); const message_id = getMessageId(stepKey, graph) ?? ''; @@ -104,6 +126,7 @@ class ModelEndHandler { } } catch (error) { logger.error('Error handling model end event:', error); + return this.finalize(errorMessage); } } } diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index c19e2c0832..2aa89bb4b1 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -764,12 +764,14 @@ class AgentClient extends BaseClient { let run; /** @type {Promise<(TAttachment | null)[] | undefined>} */ let memoryPromise; + const appConfig = this.options.req.config; + const balanceConfig = getBalanceConfig(appConfig); + const transactionsConfig = getTransactionsConfig(appConfig); try { if (!abortController) { abortController = new AbortController(); } - const appConfig = this.options.req.config; /** @type {AppConfig['endpoints']['agents']} */ const agentsEConfig = appConfig.endpoints?.[EModelEndpoint.agents]; @@ -899,31 +901,7 @@ class AgentClient extends BaseClient { ); }); } - - try { - const attachments = await this.awaitMemoryWithTimeout(memoryPromise); - if (attachments && attachments.length > 0) { - this.artifactPromises.push(...attachments); - } - - const balanceConfig = getBalanceConfig(appConfig); - const transactionsConfig = getTransactionsConfig(appConfig); - await this.recordCollectedUsage({ - context: 'message', - balance: balanceConfig, - transactions: transactionsConfig, - }); - } catch (err) { - logger.error( - '[api/server/controllers/agents/client.js #chatCompletion] Error recording collected usage', - err, - ); - } } catch (err) { - const attachments = await this.awaitMemoryWithTimeout(memoryPromise); - if (attachments && attachments.length > 0) { - this.artifactPromises.push(...attachments); - } logger.error( '[api/server/controllers/agents/client.js #sendCompletion] Operation aborted', err, @@ -938,6 +916,24 @@ class AgentClient extends BaseClient { [ContentTypes.ERROR]: `An error occurred while processing the request${err?.message ? `: ${err.message}` : ''}`, }); } + } finally { + try { + const attachments = await this.awaitMemoryWithTimeout(memoryPromise); + if (attachments && attachments.length > 0) { + this.artifactPromises.push(...attachments); + } + + await this.recordCollectedUsage({ + context: 'message', + balance: balanceConfig, + transactions: transactionsConfig, + }); + } catch (err) { + logger.error( + '[api/server/controllers/agents/client.js #chatCompletion] Error in cleanup phase', + err, + ); + } } } diff --git a/client/src/components/Messages/Content/Error.tsx b/client/src/components/Messages/Content/Error.tsx index 43da0e46e7..469e29fe32 100644 --- a/client/src/components/Messages/Content/Error.tsx +++ b/client/src/components/Messages/Content/Error.tsx @@ -43,6 +43,7 @@ const errorMessages = { [ErrorTypes.NO_BASE_URL]: 'com_error_no_base_url', [ErrorTypes.INVALID_ACTION]: `com_error_${ErrorTypes.INVALID_ACTION}`, [ErrorTypes.INVALID_REQUEST]: `com_error_${ErrorTypes.INVALID_REQUEST}`, + [ErrorTypes.REFUSAL]: 'com_error_refusal', [ErrorTypes.MISSING_MODEL]: (json: TGenericError, localize: LocalizeFunction) => { const { info: endpoint } = json; const provider = (alternateName[endpoint ?? ''] as string | undefined) ?? endpoint ?? 'unknown'; diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 834ffda0d0..cc7b952903 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -378,6 +378,7 @@ "com_error_moderation": "It appears that the content submitted has been flagged by our moderation system for not aligning with our community guidelines. We're unable to proceed with this specific topic. If you have any other questions or topics you'd like to explore, please edit your message, or create a new conversation.", "com_error_no_base_url": "No base URL found. Please provide one and try again.", "com_error_no_user_key": "No key found. Please provide a key and try again.", + "com_error_refusal": "Response refused by safety filters. Rewrite your message and try again. If you encounter this frequently while using Claude Sonnet 4.5 or Opus 4.1, you can try Sonnet 4, which has different usage restrictions.", "com_file_pages": "Pages: {{pages}}", "com_file_source": "File", "com_file_unknown": "Unknown File", diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 082f7dc4a0..f1ac91f447 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -1455,6 +1455,10 @@ export enum ErrorTypes { * Generic Authentication failure */ AUTH_FAILED = 'auth_failed', + /** + * Model refused to respond (content policy violation) + */ + REFUSAL = 'refusal', } /**