LibreChat/packages/api/src/agents/memory.spec.ts
Danny Avila 0b4deac953
🧩 fix: Missing Memory Agent Assignment for Matching IDs (#11514)
* fix: `useMemory` in AgentClient for PrelimAgent Assignment

* Updated the useMemory method in AgentClient to handle prelimAgent assignment based on memory configuration.
* Added logic to return early if prelimAgent is undefined, improving flow control.
* Introduced comprehensive unit tests to validate behavior for various memory configurations, including scenarios for matching and differing agent IDs, as well as handling of ephemeral agents.
* Mocked necessary dependencies in tests to ensure isolation and reliability of the new functionality.

* fix: Update temperature handling for Bedrock and Anthropic providers in memory management

* fix: Replace hardcoded provider strings with constants in memory agent tests

* fix: Replace hardcoded provider string with constant in allowedProviders for AgentClient

* fix: memory agent tests to use actual Providers and GraphEvents constants
2026-01-25 12:08:52 -05:00

540 lines
16 KiB
TypeScript

import { Types } from 'mongoose';
import { Run, Providers } from '@librechat/agents';
import type { IUser } from '@librechat/data-schemas';
import type { Response } from 'express';
import { processMemory } from './memory';
jest.mock('~/stream/GenerationJobManager');
const mockCreateSafeUser = jest.fn((user) => ({
id: user?.id,
email: user?.email,
name: user?.name,
username: user?.username,
}));
const mockResolveHeaders = jest.fn((opts) => {
const headers = opts.headers || {};
const user = opts.user || {};
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(headers)) {
let resolved = value as string;
resolved = resolved.replace(/\$\{(\w+)\}/g, (_match, envVar) => process.env[envVar] || '');
resolved = resolved.replace(/\{\{LIBRECHAT_USER_EMAIL\}\}/g, user.email || '');
resolved = resolved.replace(/\{\{LIBRECHAT_USER_ID\}\}/g, user.id || '');
result[key] = resolved;
}
return result;
});
jest.mock('~/utils', () => ({
Tokenizer: {
getTokenCount: jest.fn(() => 10),
},
createSafeUser: (user: unknown) => mockCreateSafeUser(user),
resolveHeaders: (opts: unknown) => mockResolveHeaders(opts),
}));
const { createSafeUser } = jest.requireMock('~/utils');
jest.mock('@librechat/agents', () => {
const actual = jest.requireActual('@librechat/agents');
return {
Run: {
create: jest.fn(() => ({
processStream: jest.fn(() => Promise.resolve('success')),
})),
},
Providers: actual.Providers,
GraphEvents: actual.GraphEvents,
};
});
function createTestUser(overrides: Partial<IUser> = {}): IUser {
return {
_id: new Types.ObjectId(),
id: new Types.ObjectId().toString(),
username: 'testuser',
email: 'test@example.com',
name: 'Test User',
avatar: 'https://example.com/avatar.png',
provider: 'email',
role: 'user',
createdAt: new Date('2021-01-01'),
updatedAt: new Date('2021-01-01'),
emailVerified: true,
...overrides,
} as IUser;
}
describe('Memory Agent Header Resolution', () => {
let testUser: IUser;
let mockRes: Response;
let mockMemoryMethods: {
setMemory: jest.Mock;
deleteMemory: jest.Mock;
getFormattedMemories: jest.Mock;
};
beforeEach(() => {
process.env.CUSTOM_API_KEY = 'sk-custom-test-key';
process.env.TEST_CUSTOM_API_KEY = 'sk-custom-test-key';
testUser = createTestUser({
id: 'user-123',
email: 'test@example.com',
});
mockRes = {
write: jest.fn(),
end: jest.fn(),
headersSent: false,
} as unknown as Response;
mockMemoryMethods = {
setMemory: jest.fn(),
deleteMemory: jest.fn(),
getFormattedMemories: jest.fn(() =>
Promise.resolve({
withKeys: 'formatted memories',
withoutKeys: 'memories without keys',
totalTokens: 100,
}),
),
};
jest.clearAllMocks();
});
afterEach(() => {
delete process.env.CUSTOM_API_KEY;
delete process.env.TEST_CUSTOM_API_KEY;
});
it('should resolve environment variables in custom endpoint headers', async () => {
const llmConfig = {
provider: 'custom',
model: 'gpt-4o-mini',
configuration: {
defaultHeaders: {
'x-custom-api-key': '${CUSTOM_API_KEY}',
'api-key': '${TEST_CUSTOM_API_KEY}',
},
},
};
await processMemory({
res: mockRes,
userId: 'user-123',
setMemory: mockMemoryMethods.setMemory,
deleteMemory: mockMemoryMethods.deleteMemory,
messages: [],
memory: 'test memory',
messageId: 'msg-123',
conversationId: 'conv-123',
validKeys: ['preferences'],
instructions: 'test instructions',
llmConfig,
user: testUser,
});
expect(Run.create as jest.Mock).toHaveBeenCalled();
const runConfig = (Run.create as jest.Mock).mock.calls[0][0];
expect(runConfig.graphConfig.llmConfig.configuration.defaultHeaders).toEqual({
'x-custom-api-key': 'sk-custom-test-key',
'api-key': 'sk-custom-test-key',
});
});
it('should resolve user placeholders in custom endpoint headers', async () => {
const llmConfig = {
provider: 'custom',
model: 'gpt-4o-mini',
configuration: {
defaultHeaders: {
'X-User-Identifier': '{{LIBRECHAT_USER_EMAIL}}',
'X-User-ID': '{{LIBRECHAT_USER_ID}}',
},
},
};
await processMemory({
res: mockRes,
userId: 'user-123',
setMemory: mockMemoryMethods.setMemory,
deleteMemory: mockMemoryMethods.deleteMemory,
messages: [],
memory: 'test memory',
messageId: 'msg-123',
conversationId: 'conv-123',
validKeys: ['preferences'],
instructions: 'test instructions',
llmConfig,
user: testUser,
});
expect(Run.create as jest.Mock).toHaveBeenCalled();
const runConfig = (Run.create as jest.Mock).mock.calls[0][0];
expect(runConfig.graphConfig.llmConfig.configuration.defaultHeaders).toEqual({
'X-User-Identifier': 'test@example.com',
'X-User-ID': 'user-123',
});
});
it('should handle mixed environment variables and user placeholders', async () => {
const llmConfig = {
provider: 'custom',
model: 'gpt-4o-mini',
configuration: {
defaultHeaders: {
'x-custom-api-key': '${CUSTOM_API_KEY}',
'X-User-Identifier': '{{LIBRECHAT_USER_EMAIL}}',
'X-Application-Identifier': 'LibreChat - Test',
},
},
};
await processMemory({
res: mockRes,
userId: 'user-123',
setMemory: mockMemoryMethods.setMemory,
deleteMemory: mockMemoryMethods.deleteMemory,
messages: [],
memory: 'test memory',
messageId: 'msg-123',
conversationId: 'conv-123',
validKeys: ['preferences'],
instructions: 'test instructions',
llmConfig,
user: testUser,
});
expect(Run.create as jest.Mock).toHaveBeenCalled();
const runConfig = (Run.create as jest.Mock).mock.calls[0][0];
expect(runConfig.graphConfig.llmConfig.configuration.defaultHeaders).toEqual({
'x-custom-api-key': 'sk-custom-test-key',
'X-User-Identifier': 'test@example.com',
'X-Application-Identifier': 'LibreChat - Test',
});
});
it('should resolve env vars when user is undefined', async () => {
const llmConfig = {
provider: 'custom',
model: 'gpt-4o-mini',
configuration: {
defaultHeaders: {
'x-custom-api-key': '${CUSTOM_API_KEY}',
},
},
};
await processMemory({
res: mockRes,
userId: 'user-123',
setMemory: mockMemoryMethods.setMemory,
deleteMemory: mockMemoryMethods.deleteMemory,
messages: [],
memory: 'test memory',
messageId: 'msg-123',
conversationId: 'conv-123',
validKeys: ['preferences'],
instructions: 'test instructions',
llmConfig,
user: undefined,
});
expect(Run.create as jest.Mock).toHaveBeenCalled();
const runConfig = (Run.create as jest.Mock).mock.calls[0][0];
expect(runConfig.graphConfig.llmConfig.configuration.defaultHeaders).toEqual({
'x-custom-api-key': 'sk-custom-test-key',
});
});
it('should not throw when llmConfig has no configuration', async () => {
const llmConfig = {
provider: Providers.OPENAI,
model: 'gpt-4o-mini',
};
await processMemory({
res: mockRes,
userId: 'user-123',
setMemory: mockMemoryMethods.setMemory,
deleteMemory: mockMemoryMethods.deleteMemory,
messages: [],
memory: 'test memory',
messageId: 'msg-123',
conversationId: 'conv-123',
validKeys: ['preferences'],
instructions: 'test instructions',
llmConfig,
user: testUser,
});
expect(Run.create as jest.Mock).toHaveBeenCalled();
const runConfig = (Run.create as jest.Mock).mock.calls[0][0];
expect(runConfig.graphConfig.llmConfig.configuration).toBeUndefined();
});
it('should use createSafeUser to sanitize user data', async () => {
const userWithSensitiveData = createTestUser({
id: 'user-123',
email: 'test@example.com',
password: 'sensitive-password',
refreshToken: 'sensitive-token',
} as unknown as Partial<IUser>);
const llmConfig = {
provider: Providers.OPENAI,
model: 'gpt-4o-mini',
configuration: {
defaultHeaders: {
'X-User-ID': '{{LIBRECHAT_USER_ID}}',
},
},
};
await processMemory({
res: mockRes,
userId: 'user-123',
setMemory: mockMemoryMethods.setMemory,
deleteMemory: mockMemoryMethods.deleteMemory,
messages: [],
memory: 'test memory',
messageId: 'msg-123',
conversationId: 'conv-123',
validKeys: ['preferences'],
instructions: 'test instructions',
llmConfig,
user: userWithSensitiveData,
});
expect(Run.create as jest.Mock).toHaveBeenCalled();
// Verify createSafeUser was used - the user object passed to Run.create should not have sensitive fields
const safeUser = createSafeUser(userWithSensitiveData);
expect(safeUser).not.toHaveProperty('password');
expect(safeUser).not.toHaveProperty('refreshToken');
expect(safeUser).toHaveProperty('id');
expect(safeUser).toHaveProperty('email');
});
it('should include instructions in user message for Bedrock provider', async () => {
const llmConfig = {
provider: Providers.BEDROCK,
model: 'us.anthropic.claude-haiku-4-5-20251001-v1:0',
};
const { HumanMessage } = await import('@langchain/core/messages');
const testMessage = new HumanMessage('test chat content');
await processMemory({
res: mockRes,
userId: 'user-123',
setMemory: mockMemoryMethods.setMemory,
deleteMemory: mockMemoryMethods.deleteMemory,
messages: [testMessage],
memory: 'existing memory',
messageId: 'msg-123',
conversationId: 'conv-123',
validKeys: ['preferences'],
instructions: 'test instructions',
llmConfig,
user: testUser,
});
expect(Run.create as jest.Mock).toHaveBeenCalled();
const runConfig = (Run.create as jest.Mock).mock.calls[0][0];
// For Bedrock, instructions should NOT be passed to graphConfig
expect(runConfig.graphConfig.instructions).toBeUndefined();
expect(runConfig.graphConfig.additional_instructions).toBeUndefined();
});
it('should pass instructions to graphConfig for non-Bedrock providers', async () => {
const llmConfig = {
provider: Providers.OPENAI,
model: 'gpt-4o-mini',
};
await processMemory({
res: mockRes,
userId: 'user-123',
setMemory: mockMemoryMethods.setMemory,
deleteMemory: mockMemoryMethods.deleteMemory,
messages: [],
memory: 'existing memory',
messageId: 'msg-123',
conversationId: 'conv-123',
validKeys: ['preferences'],
instructions: 'test instructions',
llmConfig,
user: testUser,
});
expect(Run.create as jest.Mock).toHaveBeenCalled();
const runConfig = (Run.create as jest.Mock).mock.calls[0][0];
// For non-Bedrock providers, instructions should be passed to graphConfig
expect(runConfig.graphConfig.instructions).toBe('test instructions');
expect(runConfig.graphConfig.additional_instructions).toBeDefined();
});
it('should set temperature to 1 for Bedrock with thinking enabled', async () => {
const llmConfig = {
provider: Providers.BEDROCK,
model: 'us.anthropic.claude-sonnet-4-20250514-v1:0',
temperature: 0.7,
additionalModelRequestFields: {
thinking: {
type: 'enabled',
budget_tokens: 5000,
},
},
};
await processMemory({
res: mockRes,
userId: 'user-123',
setMemory: mockMemoryMethods.setMemory,
deleteMemory: mockMemoryMethods.deleteMemory,
messages: [],
memory: 'existing memory',
messageId: 'msg-123',
conversationId: 'conv-123',
validKeys: ['preferences'],
instructions: 'test instructions',
llmConfig,
user: testUser,
});
expect(Run.create as jest.Mock).toHaveBeenCalled();
const runConfig = (Run.create as jest.Mock).mock.calls[0][0];
expect(runConfig.graphConfig.llmConfig.temperature).toBe(1);
});
it('should not modify temperature for Bedrock without thinking enabled', async () => {
const llmConfig = {
provider: Providers.BEDROCK,
model: 'us.anthropic.claude-haiku-4-5-20251001-v1:0',
temperature: 0.7,
};
await processMemory({
res: mockRes,
userId: 'user-123',
setMemory: mockMemoryMethods.setMemory,
deleteMemory: mockMemoryMethods.deleteMemory,
messages: [],
memory: 'existing memory',
messageId: 'msg-123',
conversationId: 'conv-123',
validKeys: ['preferences'],
instructions: 'test instructions',
llmConfig,
user: testUser,
});
expect(Run.create as jest.Mock).toHaveBeenCalled();
const runConfig = (Run.create as jest.Mock).mock.calls[0][0];
expect(runConfig.graphConfig.llmConfig.temperature).toBe(0.7);
});
it('should remove temperature for Anthropic with thinking enabled', async () => {
const llmConfig = {
provider: Providers.ANTHROPIC,
model: 'claude-sonnet-4-20250514',
temperature: 0.7,
thinking: {
type: 'enabled',
budget_tokens: 5000,
},
};
await processMemory({
res: mockRes,
userId: 'user-123',
setMemory: mockMemoryMethods.setMemory,
deleteMemory: mockMemoryMethods.deleteMemory,
messages: [],
memory: 'existing memory',
messageId: 'msg-123',
conversationId: 'conv-123',
validKeys: ['preferences'],
instructions: 'test instructions',
llmConfig,
user: testUser,
});
expect(Run.create as jest.Mock).toHaveBeenCalled();
const runConfig = (Run.create as jest.Mock).mock.calls[0][0];
expect(runConfig.graphConfig.llmConfig.temperature).toBeUndefined();
expect(runConfig.graphConfig.llmConfig.thinking).toEqual({
type: 'enabled',
budget_tokens: 5000,
});
});
it('should not modify temperature for Anthropic without thinking enabled', async () => {
const llmConfig = {
provider: Providers.ANTHROPIC,
model: 'claude-sonnet-4-20250514',
temperature: 0.7,
};
await processMemory({
res: mockRes,
userId: 'user-123',
setMemory: mockMemoryMethods.setMemory,
deleteMemory: mockMemoryMethods.deleteMemory,
messages: [],
memory: 'existing memory',
messageId: 'msg-123',
conversationId: 'conv-123',
validKeys: ['preferences'],
instructions: 'test instructions',
llmConfig,
user: testUser,
});
expect(Run.create as jest.Mock).toHaveBeenCalled();
const runConfig = (Run.create as jest.Mock).mock.calls[0][0];
expect(runConfig.graphConfig.llmConfig.temperature).toBe(0.7);
});
it('should not modify temperature for Anthropic with thinking type not enabled', async () => {
const llmConfig = {
provider: Providers.ANTHROPIC,
model: 'claude-sonnet-4-20250514',
temperature: 0.7,
thinking: {
type: 'disabled',
},
};
await processMemory({
res: mockRes,
userId: 'user-123',
setMemory: mockMemoryMethods.setMemory,
deleteMemory: mockMemoryMethods.deleteMemory,
messages: [],
memory: 'existing memory',
messageId: 'msg-123',
conversationId: 'conv-123',
validKeys: ['preferences'],
instructions: 'test instructions',
llmConfig,
user: testUser,
});
expect(Run.create as jest.Mock).toHaveBeenCalled();
const runConfig = (Run.create as jest.Mock).mock.calls[0][0];
expect(runConfig.graphConfig.llmConfig.temperature).toBe(0.7);
});
});