mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-10-09 06:56:30 +00:00
🛑 refactor: Demote User Abort Logs (#13904)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* fix: Demote user abort logging * fix: Handle abort causes * fix: Demote user-aborted agent completion to debug log The error users still saw originated in AgentClient's completion catch, which logged every caught error (including user aborts) at error level before checking the abort signal. Branch on abortController.signal.aborted so user-initiated aborts log at debug while real failures stay error-classified. Also give the handleAbortError it.each cases distinct titles.
This commit is contained in:
parent
cb2bafb457
commit
bc6b032421
3 changed files with 120 additions and 8 deletions
|
|
@ -1450,11 +1450,12 @@ class AgentClient extends BaseClient {
|
|||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
'[api/server/controllers/agents/client.js #sendCompletion] Operation aborted',
|
||||
err,
|
||||
);
|
||||
if (!abortController.signal.aborted) {
|
||||
if (abortController.signal.aborted) {
|
||||
logger.debug(
|
||||
'[api/server/controllers/agents/client.js #sendCompletion] Operation aborted by user',
|
||||
{ conversationId: this.conversationId, name: err?.name, code: err?.code },
|
||||
);
|
||||
} else {
|
||||
logger.error(
|
||||
'[api/server/controllers/agents/client.js #sendCompletion] Unhandled error type',
|
||||
err,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,36 @@ const { sendError } = require('~/server/middleware/error');
|
|||
const { abortRun } = require('./abortRun');
|
||||
const db = require('~/models');
|
||||
|
||||
/**
|
||||
* @param {Error | unknown} error
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isAbortError = (error) => {
|
||||
const visited = new Set();
|
||||
let current = error;
|
||||
|
||||
while (current && typeof current === 'object' && !visited.has(current)) {
|
||||
visited.add(current);
|
||||
|
||||
const errorName = current.name;
|
||||
const errorCode = current.code;
|
||||
const errorMessage = typeof current.message === 'string' ? current.message : '';
|
||||
|
||||
if (
|
||||
errorName === 'AbortError' ||
|
||||
errorCode === 'ABORT_ERR' ||
|
||||
errorMessage.includes('AbortError') ||
|
||||
/(?:operation|request|stream) was aborted/i.test(errorMessage)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
current = current.cause;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Spend tokens for all models from collected usage.
|
||||
* This handles both sequential and parallel agent execution.
|
||||
|
|
@ -200,18 +230,26 @@ const handleAbort = function () {
|
|||
* @returns {Promise<void>}
|
||||
*/
|
||||
const handleAbortError = async (res, req, error, data) => {
|
||||
const { sender, conversationId, messageId, parentMessageId, userMessageId, partialText } = data;
|
||||
|
||||
if (error?.message?.includes('base64')) {
|
||||
logger.error('[handleAbortError] Error in base64 encoding', {
|
||||
...error,
|
||||
stack: smartTruncateText(error?.stack, 1000),
|
||||
message: truncateText(error.message, 350),
|
||||
});
|
||||
} else if (isAbortError(error)) {
|
||||
logger.debug('[handleAbortError] AI response aborted by user', {
|
||||
conversationId,
|
||||
code: error?.code,
|
||||
name: error?.name,
|
||||
message: truncateText(error?.message ?? 'AbortError', 350),
|
||||
});
|
||||
} else {
|
||||
logger.error('[handleAbortError] AI response error; aborting request:', error);
|
||||
}
|
||||
const { sender, conversationId, messageId, parentMessageId, userMessageId, partialText } = data;
|
||||
|
||||
if (error.stack && error.stack.includes('google')) {
|
||||
if (error?.stack && error.stack.includes('google')) {
|
||||
logger.warn(
|
||||
`AI Response error for conversation ${conversationId} likely caused by Google censor/filter`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -73,7 +73,18 @@ jest.mock('./abortRun', () => ({
|
|||
abortRun: jest.fn(),
|
||||
}));
|
||||
|
||||
const { spendCollectedUsage } = require('./abortMiddleware');
|
||||
const { logger } = require('@librechat/data-schemas');
|
||||
const { sendError } = require('~/server/middleware/error');
|
||||
const { handleAbortError, spendCollectedUsage } = require('./abortMiddleware');
|
||||
|
||||
const buildAbortRequest = () => ({
|
||||
body: {
|
||||
model: 'gpt-4',
|
||||
},
|
||||
user: {
|
||||
id: 'user-123',
|
||||
},
|
||||
});
|
||||
|
||||
describe('abortMiddleware - spendCollectedUsage', () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -237,3 +248,65 @@ describe('abortMiddleware - spendCollectedUsage', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('abortMiddleware - handleAbortError', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
'native DOMException AbortError',
|
||||
new DOMException('The operation was aborted', 'AbortError'),
|
||||
'AbortError',
|
||||
],
|
||||
[
|
||||
'wrapped AbortError message',
|
||||
new Error('SSE stream disconnected: AbortError: The operation was aborted'),
|
||||
'Error',
|
||||
],
|
||||
[
|
||||
'cause-nested AbortError',
|
||||
new Error('Request failed', {
|
||||
cause: new DOMException('The operation was aborted', 'AbortError'),
|
||||
}),
|
||||
'Error',
|
||||
],
|
||||
])('logs a %s as a debug event instead of an error', async (_label, error, name) => {
|
||||
await handleAbortError({}, buildAbortRequest(), error, {
|
||||
sender: 'AI',
|
||||
conversationId: 'convo-123',
|
||||
messageId: 'message-123',
|
||||
parentMessageId: 'parent-123',
|
||||
userMessageId: 'user-message-123',
|
||||
});
|
||||
|
||||
expect(logger.error).not.toHaveBeenCalled();
|
||||
expect(logger.debug).toHaveBeenCalledWith('[handleAbortError] AI response aborted by user', {
|
||||
conversationId: 'convo-123',
|
||||
code: error.code,
|
||||
name,
|
||||
message: error.message,
|
||||
});
|
||||
expect(sendError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('keeps unexpected generation errors classified as errors', async () => {
|
||||
const error = new Error('Provider failed');
|
||||
|
||||
await handleAbortError({}, buildAbortRequest(), error, {
|
||||
sender: 'AI',
|
||||
conversationId: 'convo-123',
|
||||
messageId: 'message-123',
|
||||
parentMessageId: 'parent-123',
|
||||
userMessageId: 'user-message-123',
|
||||
});
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
'[handleAbortError] AI response error; aborting request:',
|
||||
error,
|
||||
);
|
||||
expect(logger.debug).not.toHaveBeenCalled();
|
||||
expect(sendError).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue