diff --git a/packages/api/src/agents/__tests__/estimateMediaTokensForMessage.spec.ts b/packages/api/src/agents/__tests__/estimateMediaTokensForMessage.spec.ts index 370168ea5d..1e4077fb9a 100644 --- a/packages/api/src/agents/__tests__/estimateMediaTokensForMessage.spec.ts +++ b/packages/api/src/agents/__tests__/estimateMediaTokensForMessage.spec.ts @@ -8,12 +8,8 @@ jest.mock('@librechat/agents', () => ({ } return null; }), - estimateAnthropicImageTokens: jest.fn( - (w: number, h: number) => Math.ceil((w * h) / 750), - ), - estimateOpenAIImageTokens: jest.fn( - (w: number, h: number) => Math.ceil((w * h) / 512) + 85, - ), + estimateAnthropicImageTokens: jest.fn((w: number, h: number) => Math.ceil((w * h) / 750)), + estimateOpenAIImageTokens: jest.fn((w: number, h: number) => Math.ceil((w * h) / 512) + 85), })); const fakeTokenCount = (text: string) => Math.ceil(text.length / 4); @@ -72,14 +68,18 @@ describe('estimateMediaTokensForMessage', () => { }); it('estimates tokens from decoded dimensions (OpenAI path)', () => { - const content = [{ type: 'image_url', image_url: 'data:image/png;base64,VALID_PNG_LONG_DATA' }]; + const content = [ + { type: 'image_url', image_url: 'data:image/png;base64,VALID_PNG_LONG_DATA' }, + ]; const result = estimateMediaTokensForMessage(content, false); expect(result).toBeGreaterThan(0); expect(result).not.toBe(1024); }); it('estimates tokens from decoded dimensions (Claude path)', () => { - const content = [{ type: 'image_url', image_url: { url: 'data:image/png;base64,VALID_PNG_LONG_DATA' } }]; + const content = [ + { type: 'image_url', image_url: { url: 'data:image/png;base64,VALID_PNG_LONG_DATA' } }, + ]; const result = estimateMediaTokensForMessage(content, true); expect(result).toBeGreaterThan(0); expect(result).not.toBe(1024); @@ -114,78 +114,92 @@ describe('estimateMediaTokensForMessage', () => { describe('document blocks - LangChain format (source_type)', () => { it('counts tokens for text source_type with getTokenCount', () => { - const content = [{ - type: 'document', - source_type: 'text', - text: 'a'.repeat(400), - }]; + const content = [ + { + type: 'document', + source_type: 'text', + text: 'a'.repeat(400), + }, + ]; expect(estimateMediaTokensForMessage(content, false, fakeTokenCount)).toBe(100); }); it('falls back to length/4 without getTokenCount', () => { - const content = [{ - type: 'document', - source_type: 'text', - text: 'a'.repeat(400), - }]; + const content = [ + { + type: 'document', + source_type: 'text', + text: 'a'.repeat(400), + }, + ]; expect(estimateMediaTokensForMessage(content, false)).toBe(100); }); it('estimates PDF pages for base64 source_type with application/pdf mime', () => { const pdfData = 'x'.repeat(150_000); - const content = [{ - type: 'document', - source_type: 'base64', - data: pdfData, - mime_type: 'application/pdf', - }]; + const content = [ + { + type: 'document', + source_type: 'base64', + data: pdfData, + mime_type: 'application/pdf', + }, + ]; const result = estimateMediaTokensForMessage(content, false); expect(result).toBe(2 * 1500); }); it('uses Claude PDF rate when isClaude is true', () => { const pdfData = 'x'.repeat(150_000); - const content = [{ - type: 'document', - source_type: 'base64', - data: pdfData, - mime_type: 'application/pdf', - }]; + const content = [ + { + type: 'document', + source_type: 'base64', + data: pdfData, + mime_type: 'application/pdf', + }, + ]; const result = estimateMediaTokensForMessage(content, true); expect(result).toBe(2 * 2000); }); it('defaults to PDF estimation for empty mime_type', () => { const pdfData = 'x'.repeat(10); - const content = [{ - type: 'document', - source_type: 'base64', - data: pdfData, - mime_type: '', - }]; + const content = [ + { + type: 'document', + source_type: 'base64', + data: pdfData, + mime_type: '', + }, + ]; const result = estimateMediaTokensForMessage(content, false); expect(result).toBe(1 * 1500); }); it('handles image/* mime inside base64 source_type', () => { - const content = [{ - type: 'document', - source_type: 'base64', - data: 'VALID_PNG', - mime_type: 'image/png', - }]; + const content = [ + { + type: 'document', + source_type: 'base64', + data: 'VALID_PNG', + mime_type: 'image/png', + }, + ]; const result = estimateMediaTokensForMessage(content, true); expect(result).toBeGreaterThan(0); expect(result).not.toBe(1024); }); it('falls back to 1024 for undecodable image in base64 source_type', () => { - const content = [{ - type: 'document', - source_type: 'base64', - data: 'BAD_DATA', - mime_type: 'image/jpeg', - }]; + const content = [ + { + type: 'document', + source_type: 'base64', + data: 'BAD_DATA', + mime_type: 'image/jpeg', + }, + ]; expect(estimateMediaTokensForMessage(content, false)).toBe(1024); }); @@ -197,50 +211,60 @@ describe('estimateMediaTokensForMessage', () => { describe('document blocks - Anthropic format (source object)', () => { it('counts tokens for text source type with getTokenCount', () => { - const content = [{ - type: 'document', - source: { type: 'text', data: 'a'.repeat(800) }, - }]; + const content = [ + { + type: 'document', + source: { type: 'text', data: 'a'.repeat(800) }, + }, + ]; expect(estimateMediaTokensForMessage(content, true, fakeTokenCount)).toBe(200); }); it('falls back to length/4 for text source without getTokenCount', () => { - const content = [{ - type: 'document', - source: { type: 'text', data: 'a'.repeat(800) }, - }]; + const content = [ + { + type: 'document', + source: { type: 'text', data: 'a'.repeat(800) }, + }, + ]; expect(estimateMediaTokensForMessage(content, true)).toBe(200); }); it('estimates PDF pages for base64 source with application/pdf', () => { const pdfData = 'x'.repeat(225_000); - const content = [{ - type: 'document', - source: { type: 'base64', data: pdfData, media_type: 'application/pdf' }, - }]; + const content = [ + { + type: 'document', + source: { type: 'base64', data: pdfData, media_type: 'application/pdf' }, + }, + ]; const result = estimateMediaTokensForMessage(content, true); expect(result).toBe(3 * 2000); }); it('returns URL fallback for url source type', () => { - const content = [{ - type: 'document', - source: { type: 'url' }, - }]; + const content = [ + { + type: 'document', + source: { type: 'url' }, + }, + ]; expect(estimateMediaTokensForMessage(content, false)).toBe(2000); }); it('handles content source type with nested images', () => { - const content = [{ - type: 'document', - source: { - type: 'content', - content: [ - { type: 'image', source: { type: 'base64', data: 'VALID_PNG' } }, - { type: 'image', source: { type: 'base64', data: 'UNDECODABLE' } }, - ], + const content = [ + { + type: 'document', + source: { + type: 'content', + content: [ + { type: 'image', source: { type: 'base64', data: 'VALID_PNG' } }, + { type: 'image', source: { type: 'base64', data: 'UNDECODABLE' } }, + ], + }, }, - }]; + ]; const result = estimateMediaTokensForMessage(content, true); expect(result).toBeGreaterThan(1024); }); @@ -253,11 +277,13 @@ describe('estimateMediaTokensForMessage', () => { describe('file blocks', () => { it('uses same logic as document for file type blocks', () => { - const content = [{ - type: 'file', - source_type: 'text', - text: 'a'.repeat(120), - }]; + const content = [ + { + type: 'file', + source_type: 'text', + text: 'a'.repeat(120), + }, + ]; expect(estimateMediaTokensForMessage(content, false, fakeTokenCount)).toBe(30); }); diff --git a/packages/api/src/agents/__tests__/initialize.test.ts b/packages/api/src/agents/__tests__/initialize.test.ts index 35a8c91ffd..eba713916f 100644 --- a/packages/api/src/agents/__tests__/initialize.test.ts +++ b/packages/api/src/agents/__tests__/initialize.test.ts @@ -236,9 +236,7 @@ function countGoogleSearchTools(tools: unknown[] | undefined): number { ); } -function countWebSearchDefinitions( - toolDefinitions: Array<{ name: string }> | undefined, -): number { +function countWebSearchDefinitions(toolDefinitions: Array<{ name: string }> | undefined): number { return ( toolDefinitions?.filter((toolDefinition) => toolDefinition.name === Tools.web_search).length ?? 0 diff --git a/packages/api/src/agents/cleanup.spec.ts b/packages/api/src/agents/cleanup.spec.ts index 563b5a67d3..19ec9ac4d0 100644 --- a/packages/api/src/agents/cleanup.spec.ts +++ b/packages/api/src/agents/cleanup.spec.ts @@ -82,13 +82,7 @@ describe('cleanCodeToolOutput', () => { 'Session files: 4 persisted file(s) are available in /mnt/data, including 1 image(s). ' + 'Use known /mnt/data paths directly in later code-tool calls. ' + 'The app displays files/images automatically; do not invent download links or wrap generated images in Markdown.'; - const input = [ - 'stdout:', - 'Report generated', - '', - 'Generated files:', - summary, - ].join('\n'); + const input = ['stdout:', 'Report generated', '', 'Generated files:', summary].join('\n'); const output = cleanCodeToolOutput(input); expect(output).toBe(input); }); diff --git a/packages/api/src/agents/usage.spec.ts b/packages/api/src/agents/usage.spec.ts index bee0c5248b..4521de8eb4 100644 --- a/packages/api/src/agents/usage.spec.ts +++ b/packages/api/src/agents/usage.spec.ts @@ -478,10 +478,10 @@ describe('recordCollectedUsage', () => { collectedUsage, }); - expect(mockSpendTokens).toHaveBeenCalledWith( - expect.anything(), - { promptTokens: 100, completionTokens: 50 }, - ); + expect(mockSpendTokens).toHaveBeenCalledWith(expect.anything(), { + promptTokens: 100, + completionTokens: 50, + }); expect(result?.output_tokens).toBe(50); }); }); diff --git a/packages/api/src/app/permissions.ts b/packages/api/src/app/permissions.ts index 129ac882a7..5c5433f427 100644 --- a/packages/api/src/app/permissions.ts +++ b/packages/api/src/app/permissions.ts @@ -151,9 +151,7 @@ export async function updateInterfacePermissions({ } else if (isMemoryDisabled) { logger.debug(`Role '${roleName}': Disabling memories as memory.disabled is true`); } else if (isMemoryReenabling) { - logger.debug( - `Role '${roleName}': Re-enabling memories due to memory configuration`, - ); + logger.debug(`Role '${roleName}': Re-enabling memories due to memory configuration`); } } else { logger.debug(`Role '${roleName}': Preserving existing permissions for '${permType}'`); diff --git a/packages/api/src/app/shutdown.spec.ts b/packages/api/src/app/shutdown.spec.ts index baabd8aa8b..2dba3d3fbe 100644 --- a/packages/api/src/app/shutdown.spec.ts +++ b/packages/api/src/app/shutdown.spec.ts @@ -71,12 +71,14 @@ describe('setupGracefulShutdown', () => { }); it('closes the server and exits 0 on SIGTERM', async () => { - const closeSpy = jest.spyOn(server, 'close').mockImplementation((cb?: (err?: Error) => void) => { - if (cb) { - setImmediate(() => cb()); - } - return server; - }); + const closeSpy = jest + .spyOn(server, 'close') + .mockImplementation((cb?: (err?: Error) => void) => { + if (cb) { + setImmediate(() => cb()); + } + return server; + }); setupGracefulShutdown(server); triggerSignal('SIGTERM'); await flush(); @@ -86,12 +88,14 @@ describe('setupGracefulShutdown', () => { }); it('closes the server and exits 0 on SIGINT', async () => { - const closeSpy = jest.spyOn(server, 'close').mockImplementation((cb?: (err?: Error) => void) => { - if (cb) { - setImmediate(() => cb()); - } - return server; - }); + const closeSpy = jest + .spyOn(server, 'close') + .mockImplementation((cb?: (err?: Error) => void) => { + if (cb) { + setImmediate(() => cb()); + } + return server; + }); setupGracefulShutdown(server); triggerSignal('SIGINT'); await flush(); diff --git a/packages/api/src/app/shutdown.ts b/packages/api/src/app/shutdown.ts index 3fc0a9d105..8ae79c979c 100644 --- a/packages/api/src/app/shutdown.ts +++ b/packages/api/src/app/shutdown.ts @@ -22,10 +22,7 @@ let httpServer: Server | null = null; * listeners in registration order and any one of them can call * `process.exit` before the HTTP server has finished closing. */ -export function registerShutdownTask( - name: string, - fn: () => void | Promise, -): void { +export function registerShutdownTask(name: string, fn: () => void | Promise): void { tasks.push({ name, fn }); } diff --git a/packages/api/src/files/rag.spec.ts b/packages/api/src/files/rag.spec.ts index 9d8ea2d4b3..4a22d4dd3e 100644 --- a/packages/api/src/files/rag.spec.ts +++ b/packages/api/src/files/rag.spec.ts @@ -1,21 +1,21 @@ jest.mock('@librechat/data-schemas', () => ({ - logger: { - debug: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }, + logger: { + debug: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, })); jest.mock('~/crypto/jwt', () => ({ - generateShortLivedToken: jest.fn().mockReturnValue('mock-jwt-token'), + generateShortLivedToken: jest.fn().mockReturnValue('mock-jwt-token'), })); jest.mock('axios', () => ({ - delete: jest.fn(), - interceptors: { - request: { use: jest.fn(), eject: jest.fn() }, - response: { use: jest.fn(), eject: jest.fn() }, - }, + delete: jest.fn(), + interceptors: { + request: { use: jest.fn(), eject: jest.fn() }, + response: { use: jest.fn(), eject: jest.fn() }, + }, })); import axios from 'axios'; @@ -26,125 +26,125 @@ import { generateShortLivedToken } from '~/crypto/jwt'; const mockedAxios = axios as jest.Mocked; const mockedLogger = logger as jest.Mocked; const mockedGenerateShortLivedToken = generateShortLivedToken as jest.MockedFunction< - typeof generateShortLivedToken + typeof generateShortLivedToken >; describe('deleteRagFile', () => { - const originalEnv = process.env; + const originalEnv = process.env; - beforeEach(() => { - jest.clearAllMocks(); - process.env = { ...originalEnv }; - process.env.RAG_API_URL = 'http://localhost:8000'; - }); + beforeEach(() => { + jest.clearAllMocks(); + process.env = { ...originalEnv }; + process.env.RAG_API_URL = 'http://localhost:8000'; + }); - afterEach(() => { - process.env = originalEnv; - }); + afterEach(() => { + process.env = originalEnv; + }); - describe('when file is embedded and RAG_API_URL is configured', () => { - it('should delete the document from RAG API successfully', async () => { - const file = { file_id: 'file-123', embedded: true }; - mockedAxios.delete.mockResolvedValueOnce({ status: 200 }); + describe('when file is embedded and RAG_API_URL is configured', () => { + it('should delete the document from RAG API successfully', async () => { + const file = { file_id: 'file-123', embedded: true }; + mockedAxios.delete.mockResolvedValueOnce({ status: 200 }); - const result = await deleteRagFile({ userId: 'user123', file }); + const result = await deleteRagFile({ userId: 'user123', file }); - expect(result).toBe(true); - expect(mockedGenerateShortLivedToken).toHaveBeenCalledWith('user123'); - expect(mockedAxios.delete).toHaveBeenCalledWith('http://localhost:8000/documents', { - headers: { - Authorization: 'Bearer mock-jwt-token', - 'Content-Type': 'application/json', - accept: 'application/json', - }, - data: ['file-123'], - }); - expect(mockedLogger.debug).toHaveBeenCalledWith( - '[deleteRagFile] Successfully deleted document file-123 from RAG API', - ); - }); + expect(result).toBe(true); + expect(mockedGenerateShortLivedToken).toHaveBeenCalledWith('user123'); + expect(mockedAxios.delete).toHaveBeenCalledWith('http://localhost:8000/documents', { + headers: { + Authorization: 'Bearer mock-jwt-token', + 'Content-Type': 'application/json', + accept: 'application/json', + }, + data: ['file-123'], + }); + expect(mockedLogger.debug).toHaveBeenCalledWith( + '[deleteRagFile] Successfully deleted document file-123 from RAG API', + ); + }); - it('should return true and log warning when document is not found (404)', async () => { - const file = { file_id: 'file-not-found', embedded: true }; - const error = new Error('Not Found') as Error & { response?: { status?: number } }; - error.response = { status: 404 }; - mockedAxios.delete.mockRejectedValueOnce(error); + it('should return true and log warning when document is not found (404)', async () => { + const file = { file_id: 'file-not-found', embedded: true }; + const error = new Error('Not Found') as Error & { response?: { status?: number } }; + error.response = { status: 404 }; + mockedAxios.delete.mockRejectedValueOnce(error); - const result = await deleteRagFile({ userId: 'user123', file }); + const result = await deleteRagFile({ userId: 'user123', file }); - expect(result).toBe(true); - expect(mockedLogger.warn).toHaveBeenCalledWith( - '[deleteRagFile] Document file-not-found not found in RAG API, may have been deleted already', - ); - }); + expect(result).toBe(true); + expect(mockedLogger.warn).toHaveBeenCalledWith( + '[deleteRagFile] Document file-not-found not found in RAG API, may have been deleted already', + ); + }); - it('should return false and log error on other errors', async () => { - const file = { file_id: 'file-error', embedded: true }; - const error = new Error('Server Error') as Error & { response?: { status?: number } }; - error.response = { status: 500 }; - mockedAxios.delete.mockRejectedValueOnce(error); + it('should return false and log error on other errors', async () => { + const file = { file_id: 'file-error', embedded: true }; + const error = new Error('Server Error') as Error & { response?: { status?: number } }; + error.response = { status: 500 }; + mockedAxios.delete.mockRejectedValueOnce(error); - const result = await deleteRagFile({ userId: 'user123', file }); + const result = await deleteRagFile({ userId: 'user123', file }); - expect(result).toBe(false); - expect(mockedLogger.error).toHaveBeenCalledWith( - '[deleteRagFile] Error deleting document from RAG API:', - 'Server Error', - ); - }); - }); + expect(result).toBe(false); + expect(mockedLogger.error).toHaveBeenCalledWith( + '[deleteRagFile] Error deleting document from RAG API:', + 'Server Error', + ); + }); + }); - describe('when file is not embedded', () => { - it('should skip RAG deletion and return true', async () => { - const file = { file_id: 'file-123', embedded: false }; + describe('when file is not embedded', () => { + it('should skip RAG deletion and return true', async () => { + const file = { file_id: 'file-123', embedded: false }; - const result = await deleteRagFile({ userId: 'user123', file }); + const result = await deleteRagFile({ userId: 'user123', file }); - expect(result).toBe(true); - expect(mockedAxios.delete).not.toHaveBeenCalled(); - expect(mockedGenerateShortLivedToken).not.toHaveBeenCalled(); - }); + expect(result).toBe(true); + expect(mockedAxios.delete).not.toHaveBeenCalled(); + expect(mockedGenerateShortLivedToken).not.toHaveBeenCalled(); + }); - it('should skip RAG deletion when embedded is undefined', async () => { - const file = { file_id: 'file-123' }; + it('should skip RAG deletion when embedded is undefined', async () => { + const file = { file_id: 'file-123' }; - const result = await deleteRagFile({ userId: 'user123', file }); + const result = await deleteRagFile({ userId: 'user123', file }); - expect(result).toBe(true); - expect(mockedAxios.delete).not.toHaveBeenCalled(); - }); - }); + expect(result).toBe(true); + expect(mockedAxios.delete).not.toHaveBeenCalled(); + }); + }); - describe('when RAG_API_URL is not configured', () => { - it('should skip RAG deletion and return true', async () => { - delete process.env.RAG_API_URL; - const file = { file_id: 'file-123', embedded: true }; + describe('when RAG_API_URL is not configured', () => { + it('should skip RAG deletion and return true', async () => { + delete process.env.RAG_API_URL; + const file = { file_id: 'file-123', embedded: true }; - const result = await deleteRagFile({ userId: 'user123', file }); + const result = await deleteRagFile({ userId: 'user123', file }); - expect(result).toBe(true); - expect(mockedAxios.delete).not.toHaveBeenCalled(); - }); - }); + expect(result).toBe(true); + expect(mockedAxios.delete).not.toHaveBeenCalled(); + }); + }); - describe('userId handling', () => { - it('should return false when no userId is provided', async () => { - const file = { file_id: 'file-123', embedded: true }; + describe('userId handling', () => { + it('should return false when no userId is provided', async () => { + const file = { file_id: 'file-123', embedded: true }; - const result = await deleteRagFile({ userId: '', file }); + const result = await deleteRagFile({ userId: '', file }); - expect(result).toBe(false); - expect(mockedLogger.error).toHaveBeenCalledWith('[deleteRagFile] No user ID provided'); - expect(mockedAxios.delete).not.toHaveBeenCalled(); - }); + expect(result).toBe(false); + expect(mockedLogger.error).toHaveBeenCalledWith('[deleteRagFile] No user ID provided'); + expect(mockedAxios.delete).not.toHaveBeenCalled(); + }); - it('should return false when userId is undefined', async () => { - const file = { file_id: 'file-123', embedded: true }; + it('should return false when userId is undefined', async () => { + const file = { file_id: 'file-123', embedded: true }; - const result = await deleteRagFile({ userId: undefined as unknown as string, file }); + const result = await deleteRagFile({ userId: undefined as unknown as string, file }); - expect(result).toBe(false); - expect(mockedLogger.error).toHaveBeenCalledWith('[deleteRagFile] No user ID provided'); - }); - }); + expect(result).toBe(false); + expect(mockedLogger.error).toHaveBeenCalledWith('[deleteRagFile] No user ID provided'); + }); + }); }); diff --git a/packages/api/src/files/rag.ts b/packages/api/src/files/rag.ts index 7155f62c12..7876cda56d 100644 --- a/packages/api/src/files/rag.ts +++ b/packages/api/src/files/rag.ts @@ -3,13 +3,13 @@ import { logger } from '@librechat/data-schemas'; import { generateShortLivedToken } from '~/crypto/jwt'; interface DeleteRagFileParams { - /** The user ID. Required for authentication. If not provided, the function returns false and logs an error. */ - userId: string; - /** The file object. Must have `embedded` and `file_id` properties. */ - file: { - file_id: string; - embedded?: boolean; - }; + /** The user ID. Required for authentication. If not provided, the function returns false and logs an error. */ + userId: string; + /** The file object. Must have `embedded` and `file_id` properties. */ + file: { + file_id: string; + embedded?: boolean; + }; } /** @@ -23,38 +23,38 @@ interface DeleteRagFileParams { * @returns Returns true if deletion was successful or skipped, false if there was an error. */ export async function deleteRagFile({ userId, file }: DeleteRagFileParams): Promise { - if (!file.embedded || !process.env.RAG_API_URL) { - return true; - } + if (!file.embedded || !process.env.RAG_API_URL) { + return true; + } - if (!userId) { - logger.error('[deleteRagFile] No user ID provided'); - return false; - } + if (!userId) { + logger.error('[deleteRagFile] No user ID provided'); + return false; + } - const jwtToken = generateShortLivedToken(userId); + const jwtToken = generateShortLivedToken(userId); - try { - await axios.delete(`${process.env.RAG_API_URL}/documents`, { - headers: { - Authorization: `Bearer ${jwtToken}`, - 'Content-Type': 'application/json', - accept: 'application/json', - }, - data: [file.file_id], - }); - logger.debug(`[deleteRagFile] Successfully deleted document ${file.file_id} from RAG API`); - return true; - } catch (error) { - const axiosError = error as { response?: { status?: number }; message?: string }; - if (axiosError.response?.status === 404) { - logger.warn( - `[deleteRagFile] Document ${file.file_id} not found in RAG API, may have been deleted already`, - ); - return true; - } else { - logger.error('[deleteRagFile] Error deleting document from RAG API:', axiosError.message); - return false; - } - } + try { + await axios.delete(`${process.env.RAG_API_URL}/documents`, { + headers: { + Authorization: `Bearer ${jwtToken}`, + 'Content-Type': 'application/json', + accept: 'application/json', + }, + data: [file.file_id], + }); + logger.debug(`[deleteRagFile] Successfully deleted document ${file.file_id} from RAG API`); + return true; + } catch (error) { + const axiosError = error as { response?: { status?: number }; message?: string }; + if (axiosError.response?.status === 404) { + logger.warn( + `[deleteRagFile] Document ${file.file_id} not found in RAG API, may have been deleted already`, + ); + return true; + } else { + logger.error('[deleteRagFile] Error deleting document from RAG API:', axiosError.message); + return false; + } + } } diff --git a/packages/api/src/middleware/tenant.ts b/packages/api/src/middleware/tenant.ts index e194e42cd5..52f89a02d7 100644 --- a/packages/api/src/middleware/tenant.ts +++ b/packages/api/src/middleware/tenant.ts @@ -260,11 +260,7 @@ export function restoreTenantContextFromReq( logger.warn('[restoreTenantContextFromReq] Rejected system tenant for request route', { path: req.path, }); - return rejectRequestWithUploadCleanup( - req, - res, - SYSTEM_TENANT_REJECTION_MESSAGE, - ); + return rejectRequestWithUploadCleanup(req, res, SYSTEM_TENANT_REJECTION_MESSAGE); } const currentContext = tenantStorage.getStore(); diff --git a/packages/api/src/telemetry/sdk.spec.ts b/packages/api/src/telemetry/sdk.spec.ts index 32b33cb566..f43762237a 100644 --- a/packages/api/src/telemetry/sdk.spec.ts +++ b/packages/api/src/telemetry/sdk.spec.ts @@ -512,9 +512,8 @@ describe('telemetry SDK lifecycle', () => { expect(taskFn).toBeDefined(); await taskFn?.(); - expect(emitWarningSpy).toHaveBeenCalledWith( - 'OpenTelemetry shutdown failed: flush failed', - { code: 'LIBRECHAT_OTEL' }, - ); + expect(emitWarningSpy).toHaveBeenCalledWith('OpenTelemetry shutdown failed: flush failed', { + code: 'LIBRECHAT_OTEL', + }); }); }); diff --git a/packages/api/src/telemetry/stream.spec.ts b/packages/api/src/telemetry/stream.spec.ts index 460ef40078..331384256c 100644 --- a/packages/api/src/telemetry/stream.spec.ts +++ b/packages/api/src/telemetry/stream.spec.ts @@ -72,16 +72,20 @@ describe('createSseStreamTelemetry', () => { res.writableEnded = true; res.emit('finish'); - expect(startSpan).toHaveBeenCalledWith('librechat.sse.stream', { - kind: SpanKind.INTERNAL, - attributes: expect.objectContaining({ - 'http.request.method': 'GET', - 'http.route': '/api/agents/chat/stream/:streamId', - 'librechat.stream.id': 'stream-1', - 'librechat.stream.resume': false, - 'librechat.stream.route': '/api/agents/chat/stream/:streamId', - }), - }, context.active()); + expect(startSpan).toHaveBeenCalledWith( + 'librechat.sse.stream', + { + kind: SpanKind.INTERNAL, + attributes: expect.objectContaining({ + 'http.request.method': 'GET', + 'http.route': '/api/agents/chat/stream/:streamId', + 'librechat.stream.id': 'stream-1', + 'librechat.stream.resume': false, + 'librechat.stream.route': '/api/agents/chat/stream/:streamId', + }), + }, + context.active(), + ); expect(span.addEvent).toHaveBeenCalledWith('headers_flushed'); expect(span.addEvent).toHaveBeenCalledWith('first_chunk'); expect(span.addEvent).toHaveBeenCalledWith('final_event_emitted'); diff --git a/packages/api/src/telemetry/stream.ts b/packages/api/src/telemetry/stream.ts index d696f557c3..0ef07d57f1 100644 --- a/packages/api/src/telemetry/stream.ts +++ b/packages/api/src/telemetry/stream.ts @@ -37,16 +37,20 @@ class SseStreamSpanTelemetry implements SseStreamTelemetry { private plannedEndReason: StreamEndReason | undefined; constructor({ isResume, req, res, streamId }: SseStreamTelemetryOptions) { - this.span = trace.getTracer('librechat.telemetry').startSpan(STREAM_SPAN_NAME, { - kind: SpanKind.INTERNAL, - attributes: { - 'http.request.method': req.method, - 'http.route': STREAM_ROUTE, - 'librechat.stream.id': streamId, - 'librechat.stream.resume': isResume, - 'librechat.stream.route': STREAM_ROUTE, + this.span = trace.getTracer('librechat.telemetry').startSpan( + STREAM_SPAN_NAME, + { + kind: SpanKind.INTERNAL, + attributes: { + 'http.request.method': req.method, + 'http.route': STREAM_ROUTE, + 'librechat.stream.id': streamId, + 'librechat.stream.resume': isResume, + 'librechat.stream.route': STREAM_ROUTE, + }, }, - }, context.active()); + context.active(), + ); res.once('finish', () => { this.end(this.plannedEndReason ?? (this.errorEventEmitted ? 'server_error' : 'done')); @@ -138,8 +142,6 @@ class SseStreamSpanTelemetry implements SseStreamTelemetry { } } -export function createSseStreamTelemetry( - options: SseStreamTelemetryOptions, -): SseStreamTelemetry { +export function createSseStreamTelemetry(options: SseStreamTelemetryOptions): SseStreamTelemetry { return new SseStreamSpanTelemetry(options); } diff --git a/packages/api/src/utils/graph.ts b/packages/api/src/utils/graph.ts index 0ff3fc3583..3bb3d2bf4c 100644 --- a/packages/api/src/utils/graph.ts +++ b/packages/api/src/utils/graph.ts @@ -11,10 +11,7 @@ import { * Pre-computed regex for matching the Graph token placeholder. * Escapes curly braces in the placeholder string for safe regex use. */ -const GRAPH_TOKEN_REGEX = new RegExp( - GRAPH_TOKEN_PLACEHOLDER.replace(/[{}]/g, '\\$&'), - 'g', -); +const GRAPH_TOKEN_REGEX = new RegExp(GRAPH_TOKEN_PLACEHOLDER.replace(/[{}]/g, '\\$&'), 'g'); /** * Response from a Graph API token exchange. @@ -143,7 +140,9 @@ export async function resolveGraphTokenPlaceholder( return value.replace(GRAPH_TOKEN_REGEX, graphTokenResponse.access_token); } - logger.warn('[resolveGraphTokenPlaceholder] Graph token exchange did not return an access token'); + logger.warn( + '[resolveGraphTokenPlaceholder] Graph token exchange did not return an access token', + ); return value; } catch (error) { logger.error('[resolveGraphTokenPlaceholder] Failed to exchange token for Graph API:', error); @@ -185,14 +184,13 @@ export async function resolveGraphTokensInRecord( * @param graphOptions - Options for Graph token resolution * @returns The options with Graph token placeholders resolved */ -export async function preProcessGraphTokens; - env?: Record; - url?: string; -}>( - options: T, - graphOptions: GraphTokenOptions, -): Promise { +export async function preProcessGraphTokens< + T extends { + headers?: Record; + env?: Record; + url?: string; + }, +>(options: T, graphOptions: GraphTokenOptions): Promise { if (!mcpOptionsContainGraphTokenPlaceholder(options)) { return options; } diff --git a/packages/client/src/theme/context/ThemeProvider.tsx b/packages/client/src/theme/context/ThemeProvider.tsx index 30773d222a..4e654b0464 100644 --- a/packages/client/src/theme/context/ThemeProvider.tsx +++ b/packages/client/src/theme/context/ThemeProvider.tsx @@ -1,4 +1,12 @@ -import React, { createContext, useContext, useEffect, useMemo, useCallback, useState, useRef } from 'react'; +import React, { + createContext, + useContext, + useEffect, + useMemo, + useCallback, + useState, + useRef, +} from 'react'; import { IThemeRGB } from '../types'; import applyTheme from '../utils/applyTheme'; diff --git a/packages/data-schemas/src/config/tenantContext.spec.ts b/packages/data-schemas/src/config/tenantContext.spec.ts index a01e01ff76..f82c41699c 100644 --- a/packages/data-schemas/src/config/tenantContext.spec.ts +++ b/packages/data-schemas/src/config/tenantContext.spec.ts @@ -1,4 +1,10 @@ -import { tenantStorage, getUserId, getRequestId, runAsSystem, scopedCacheKey } from './tenantContext'; +import { + tenantStorage, + getUserId, + getRequestId, + runAsSystem, + scopedCacheKey, +} from './tenantContext'; describe('scopedCacheKey', () => { it('returns base key when no ALS context is set', () => { @@ -32,12 +38,15 @@ describe('scopedCacheKey', () => { }); it('preserves user and request context inside system tenant operations', async () => { - await tenantStorage.run({ tenantId: 'acme', userId: 'user-1', requestId: 'req-1' }, async () => { - await runAsSystem(async () => { - expect(getUserId()).toBe('user-1'); - expect(getRequestId()).toBe('req-1'); - expect(scopedCacheKey('KEY')).toBe('KEY'); - }); - }); + await tenantStorage.run( + { tenantId: 'acme', userId: 'user-1', requestId: 'req-1' }, + async () => { + await runAsSystem(async () => { + expect(getUserId()).toBe('user-1'); + expect(getRequestId()).toBe('req-1'); + expect(scopedCacheKey('KEY')).toBe('KEY'); + }); + }, + ); }); }); diff --git a/packages/data-schemas/src/methods/prompt.getPromptGroup.spec.ts b/packages/data-schemas/src/methods/prompt.getPromptGroup.spec.ts index 3d3b6a74bd..00f0496736 100644 --- a/packages/data-schemas/src/methods/prompt.getPromptGroup.spec.ts +++ b/packages/data-schemas/src/methods/prompt.getPromptGroup.spec.ts @@ -73,14 +73,16 @@ function collectRecords(value: unknown, predicate: (record: Record { - const stage = record.stage; - if (stage === 'IDHACK' || stage === 'EXPRESS_IXSCAN') return true; - if (stage !== 'IXSCAN') return false; + return ( + collectRecords(explain, (record) => { + const stage = record.stage; + if (stage === 'IDHACK' || stage === 'EXPRESS_IXSCAN') return true; + if (stage !== 'IXSCAN') return false; - const keyPattern = record.keyPattern; - return isRecord(keyPattern) && keyPattern._id === 1; - }).length > 0; + const keyPattern = record.keyPattern; + return isRecord(keyPattern) && keyPattern._id === 1; + }).length > 0 + ); } beforeAll(async () => { diff --git a/packages/data-schemas/src/methods/userGroup.methods.spec.ts b/packages/data-schemas/src/methods/userGroup.methods.spec.ts index 51848de091..abb3a1f948 100644 --- a/packages/data-schemas/src/methods/userGroup.methods.spec.ts +++ b/packages/data-schemas/src/methods/userGroup.methods.spec.ts @@ -716,10 +716,7 @@ describe('UserGroup Methods - Detailed Tests', () => { memberIds: ['m1', 'm2', 'm3'], }); - const updated = await methods.removeMemberById( - group._id as mongoose.Types.ObjectId, - 'm2', - ); + const updated = await methods.removeMemberById(group._id as mongoose.Types.ObjectId, 'm2'); expect(updated).toBeDefined(); expect(updated?.memberIds).toEqual(['m1', 'm3']); diff --git a/packages/data-schemas/src/utils/retention.ts b/packages/data-schemas/src/utils/retention.ts index c25896fbf9..7d57351676 100644 --- a/packages/data-schemas/src/utils/retention.ts +++ b/packages/data-schemas/src/utils/retention.ts @@ -15,8 +15,7 @@ export const activeExpirationFilter = < export const legacyPermanentExpirationFilter = < T extends RetentionFilterDocument = RetentionFilterDocument, ->(): FilterQuery => - ({ expiredAt: null }) as FilterQuery; +>(): FilterQuery => ({ expiredAt: null }) as FilterQuery; export const buildRetentionVisibilityFilter = < T extends RetentionFilterDocument = RetentionFilterDocument,