🛡️ feat: Add Model Refusal Error Handling (Anthropic) (#10478)

* 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
This commit is contained in:
Danny Avila 2025-11-13 08:34:55 -05:00 committed by GitHub
parent 3f62ce054f
commit 524fc5bae4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 54 additions and 29 deletions

View file

@ -1,7 +1,7 @@
const { nanoid } = require('nanoid'); const { nanoid } = require('nanoid');
const { sendEvent } = require('@librechat/api'); const { sendEvent } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas'); const { logger } = require('@librechat/data-schemas');
const { Tools, StepTypes, FileContext } = require('librechat-data-provider'); const { Tools, StepTypes, FileContext, ErrorTypes } = require('librechat-data-provider');
const { const {
EnvVar, EnvVar,
Providers, Providers,
@ -27,6 +27,13 @@ class ModelEndHandler {
this.collectedUsage = collectedUsage; this.collectedUsage = collectedUsage;
} }
finalize(errorMessage) {
if (!errorMessage) {
return;
}
throw new Error(errorMessage);
}
/** /**
* @param {string} event * @param {string} event
* @param {ModelEndData | undefined} data * @param {ModelEndData | undefined} data
@ -40,10 +47,25 @@ class ModelEndHandler {
return; return;
} }
/** @type {string | undefined} */
let errorMessage;
try { try {
const agentContext = graph.getAgentContext(metadata); const agentContext = graph.getAgentContext(metadata);
const isGoogle = agentContext.provider === Providers.GOOGLE; const isGoogle = agentContext.provider === Providers.GOOGLE;
const streamingDisabled = !!agentContext.clientOptions?.disableStreaming; 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; const toolCalls = data?.output?.tool_calls;
let hasUnprocessedToolCalls = false; let hasUnprocessedToolCalls = false;
@ -62,7 +84,7 @@ class ModelEndHandler {
const usage = data?.output?.usage_metadata; const usage = data?.output?.usage_metadata;
if (!usage) { if (!usage) {
return; return this.finalize(errorMessage);
} }
const modelName = metadata?.ls_model_name || agentContext.clientOptions?.model; const modelName = metadata?.ls_model_name || agentContext.clientOptions?.model;
if (modelName) { if (modelName) {
@ -71,10 +93,10 @@ class ModelEndHandler {
this.collectedUsage.push(usage); this.collectedUsage.push(usage);
if (!streamingDisabled) { if (!streamingDisabled) {
return; return this.finalize(errorMessage);
} }
if (!data.output.content) { if (!data.output.content) {
return; return this.finalize(errorMessage);
} }
const stepKey = graph.getStepKey(metadata); const stepKey = graph.getStepKey(metadata);
const message_id = getMessageId(stepKey, graph) ?? ''; const message_id = getMessageId(stepKey, graph) ?? '';
@ -104,6 +126,7 @@ class ModelEndHandler {
} }
} catch (error) { } catch (error) {
logger.error('Error handling model end event:', error); logger.error('Error handling model end event:', error);
return this.finalize(errorMessage);
} }
} }
} }

View file

@ -764,12 +764,14 @@ class AgentClient extends BaseClient {
let run; let run;
/** @type {Promise<(TAttachment | null)[] | undefined>} */ /** @type {Promise<(TAttachment | null)[] | undefined>} */
let memoryPromise; let memoryPromise;
const appConfig = this.options.req.config;
const balanceConfig = getBalanceConfig(appConfig);
const transactionsConfig = getTransactionsConfig(appConfig);
try { try {
if (!abortController) { if (!abortController) {
abortController = new AbortController(); abortController = new AbortController();
} }
const appConfig = this.options.req.config;
/** @type {AppConfig['endpoints']['agents']} */ /** @type {AppConfig['endpoints']['agents']} */
const agentsEConfig = appConfig.endpoints?.[EModelEndpoint.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) { } catch (err) {
const attachments = await this.awaitMemoryWithTimeout(memoryPromise);
if (attachments && attachments.length > 0) {
this.artifactPromises.push(...attachments);
}
logger.error( logger.error(
'[api/server/controllers/agents/client.js #sendCompletion] Operation aborted', '[api/server/controllers/agents/client.js #sendCompletion] Operation aborted',
err, err,
@ -938,6 +916,24 @@ class AgentClient extends BaseClient {
[ContentTypes.ERROR]: `An error occurred while processing the request${err?.message ? `: ${err.message}` : ''}`, [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,
);
}
} }
} }

View file

@ -43,6 +43,7 @@ const errorMessages = {
[ErrorTypes.NO_BASE_URL]: 'com_error_no_base_url', [ErrorTypes.NO_BASE_URL]: 'com_error_no_base_url',
[ErrorTypes.INVALID_ACTION]: `com_error_${ErrorTypes.INVALID_ACTION}`, [ErrorTypes.INVALID_ACTION]: `com_error_${ErrorTypes.INVALID_ACTION}`,
[ErrorTypes.INVALID_REQUEST]: `com_error_${ErrorTypes.INVALID_REQUEST}`, [ErrorTypes.INVALID_REQUEST]: `com_error_${ErrorTypes.INVALID_REQUEST}`,
[ErrorTypes.REFUSAL]: 'com_error_refusal',
[ErrorTypes.MISSING_MODEL]: (json: TGenericError, localize: LocalizeFunction) => { [ErrorTypes.MISSING_MODEL]: (json: TGenericError, localize: LocalizeFunction) => {
const { info: endpoint } = json; const { info: endpoint } = json;
const provider = (alternateName[endpoint ?? ''] as string | undefined) ?? endpoint ?? 'unknown'; const provider = (alternateName[endpoint ?? ''] as string | undefined) ?? endpoint ?? 'unknown';

View file

@ -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_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_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_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_pages": "Pages: {{pages}}",
"com_file_source": "File", "com_file_source": "File",
"com_file_unknown": "Unknown File", "com_file_unknown": "Unknown File",

View file

@ -1455,6 +1455,10 @@ export enum ErrorTypes {
* Generic Authentication failure * Generic Authentication failure
*/ */
AUTH_FAILED = 'auth_failed', AUTH_FAILED = 'auth_failed',
/**
* Model refused to respond (content policy violation)
*/
REFUSAL = 'refusal',
} }
/** /**