mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-04 06:52:47 +00:00
🎨 chore: prettier --write all workspaces (#13281)
Run `prettier --write` over the source trees of every workspace to align
with the repo's own `.prettierrc` (`printWidth: 100`, `singleQuote: true`,
`trailingComma: 'all'`, etc.). **19 files reformatted total** — purely
whitespace and line-wrap changes, no functional edits and no API changes.
Scope:
- `packages/api/src/**/*.{ts,tsx}` — 14 files
- `packages/client/src/**/*.{ts,tsx}` — 1 file
- `packages/data-schemas/src/**/*.{ts,tsx}` — 4 files
- `api/**`, `client/**`, `packages/data-provider/**` — already prettier-clean
Most of the drift is in argument-list / type-annotation wrapping where
the formatted form fits within `printWidth` but the current source keeps
a hand-wrapped multi-line shape. Example:
// before
function countWebSearchDefinitions(
toolDefinitions: Array<{ name: string }> | undefined,
): number { … }
// after (still well under 100 cols)
function countWebSearchDefinitions(toolDefinitions: Array<{ name: string }> | undefined): number { … }
`npx prettier --check` across all workspaces is now clean. The local
pre-commit hook (`lint-staged` → `prettier --write`) would have produced
the same result on any future edit to these files.
There are no prettier-checking workflows in CI today, so drift like this
can re-appear if PRs are merged with the hook bypassed. Companion PR
#13282 adds a `prettier --check` step to `eslint-ci.yml` so future
drift gets caught.
This commit is contained in:
parent
2bcf3e8582
commit
83c7d637c3
19 changed files with 349 additions and 318 deletions
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}'`);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
): void {
|
||||
export function registerShutdownTask(name: string, fn: () => void | Promise<void>): void {
|
||||
tasks.push({ name, fn });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<typeof axios>;
|
||||
const mockedLogger = logger as jest.Mocked<typeof logger>;
|
||||
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<boolean> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T extends {
|
||||
headers?: Record<string, string>;
|
||||
env?: Record<string, string>;
|
||||
url?: string;
|
||||
}>(
|
||||
options: T,
|
||||
graphOptions: GraphTokenOptions,
|
||||
): Promise<T> {
|
||||
export async function preProcessGraphTokens<
|
||||
T extends {
|
||||
headers?: Record<string, string>;
|
||||
env?: Record<string, string>;
|
||||
url?: string;
|
||||
},
|
||||
>(options: T, graphOptions: GraphTokenOptions): Promise<T> {
|
||||
if (!mcpOptionsContainGraphTokenPlaceholder(options)) {
|
||||
return options;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -73,14 +73,16 @@ function collectRecords(value: unknown, predicate: (record: Record<string, unkno
|
|||
}
|
||||
|
||||
function hasIndexedIdPlan(explain: unknown): boolean {
|
||||
return collectRecords(explain, (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 () => {
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -15,8 +15,7 @@ export const activeExpirationFilter = <
|
|||
|
||||
export const legacyPermanentExpirationFilter = <
|
||||
T extends RetentionFilterDocument = RetentionFilterDocument,
|
||||
>(): FilterQuery<T> =>
|
||||
({ expiredAt: null }) as FilterQuery<T>;
|
||||
>(): FilterQuery<T> => ({ expiredAt: null }) as FilterQuery<T>;
|
||||
|
||||
export const buildRetentionVisibilityFilter = <
|
||||
T extends RetentionFilterDocument = RetentionFilterDocument,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue