Skip to content
5 changes: 2 additions & 3 deletions api/models/Conversation.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { logger } = require('@librechat/data-schemas');
const { getMessages, deleteMessages } = require('./Message');
const { Conversation } = require('~/db/models');
const { createTempChatExpirationDate } = require('~/server/utils/tempChatRetention');

/**
* Searches for a conversation by conversationId and returns a lean document with only conversationId and user.
Expand Down Expand Up @@ -99,9 +100,7 @@ module.exports = {
}

if (req.body.isTemporary) {
const expiredAt = new Date();
expiredAt.setDate(expiredAt.getDate() + 30);
update.expiredAt = expiredAt;
update.expiredAt = createTempChatExpirationDate(req.app.locals?.config);
} else {
update.expiredAt = null;
}
Expand Down
5 changes: 2 additions & 3 deletions api/models/Message.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { z } = require('zod');
const { logger } = require('@librechat/data-schemas');
const { Message } = require('~/db/models');
const { createTempChatExpirationDate } = require('~/server/utils/tempChatRetention');

const idSchema = z.string().uuid();

Expand Down Expand Up @@ -54,9 +55,7 @@ async function saveMessage(req, params, metadata) {
};

if (req?.body?.isTemporary) {
const expiredAt = new Date();
expiredAt.setDate(expiredAt.getDate() + 30);
update.expiredAt = expiredAt;
update.expiredAt = createTempChatExpirationDate(req.app.locals?.config);
} else {
update.expiredAt = null;
}
Expand Down
1 change: 1 addition & 0 deletions api/server/services/AppService.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ const AppService = async (app) => {
interfaceConfig,
turnstileConfig,
balance,
config,
};

const agentsDefaults = agentsConfigSetup(config);
Expand Down
7 changes: 7 additions & 0 deletions api/server/services/AppService.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,13 @@ describe('AppService', () => {
disableBuilder: false,
capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
},
config: {
registration: { socialLogins: ['testLogin'] },
fileStrategy: 'testStrategy',
balance: {
enabled: true,
},
},
});
});

Expand Down
132 changes: 132 additions & 0 deletions api/server/utils/__tests__/tempChatRetention.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
const {
DEFAULT_RETENTION_DAYS,
MIN_RETENTION_DAYS,
MAX_RETENTION_DAYS,
getTempChatRetentionDays,
createTempChatExpirationDate,
} = require('../tempChatRetention');

describe('tempChatRetention', () => {
const originalEnv = process.env;

beforeEach(() => {
jest.resetModules();
process.env = { ...originalEnv };
delete process.env.TEMP_CHAT_RETENTION_DAYS;
});

afterAll(() => {
process.env = originalEnv;
});

describe('getTempChatRetentionDays', () => {
it('should return default retention days when no config or env var is set', () => {
const result = getTempChatRetentionDays();
expect(result).toBe(DEFAULT_RETENTION_DAYS);
});

it('should use environment variable when set', () => {
process.env.TEMP_CHAT_RETENTION_DAYS = '15';
const result = getTempChatRetentionDays();
expect(result).toBe(15);
});

it('should use config value when set', () => {
const config = {
interface: {
temporaryChatRetentionDays: 7,
},
};
const result = getTempChatRetentionDays(config);
expect(result).toBe(7);
});

it('should prioritize config over environment variable', () => {
process.env.TEMP_CHAT_RETENTION_DAYS = '15';
const config = {
interface: {
temporaryChatRetentionDays: 7,
},
};
const result = getTempChatRetentionDays(config);
expect(result).toBe(7);
});

it('should enforce minimum retention period', () => {
const config = {
interface: {
temporaryChatRetentionDays: 0,
},
};
const result = getTempChatRetentionDays(config);
expect(result).toBe(MIN_RETENTION_DAYS);
});

it('should enforce maximum retention period', () => {
const config = {
interface: {
temporaryChatRetentionDays: 400,
},
};
const result = getTempChatRetentionDays(config);
expect(result).toBe(MAX_RETENTION_DAYS);
});

it('should handle invalid environment variable', () => {
process.env.TEMP_CHAT_RETENTION_DAYS = 'invalid';
const result = getTempChatRetentionDays();
expect(result).toBe(DEFAULT_RETENTION_DAYS);
});

it('should handle invalid config value', () => {
const config = {
interface: {
temporaryChatRetentionDays: 'invalid',
},
};
const result = getTempChatRetentionDays(config);
expect(result).toBe(DEFAULT_RETENTION_DAYS);
});
});

describe('createTempChatExpirationDate', () => {
it('should create expiration date with default retention period', () => {
const result = createTempChatExpirationDate();

const expectedDate = new Date();
expectedDate.setDate(expectedDate.getDate() + DEFAULT_RETENTION_DAYS);

// Allow for small time differences in test execution
const timeDiff = Math.abs(result.getTime() - expectedDate.getTime());
expect(timeDiff).toBeLessThan(1000); // Less than 1 second difference
});

it('should create expiration date with custom retention period', () => {
const config = {
interface: {
temporaryChatRetentionDays: 7,
},
};

const result = createTempChatExpirationDate(config);

const expectedDate = new Date();
expectedDate.setDate(expectedDate.getDate() + 7);

// Allow for small time differences in test execution
const timeDiff = Math.abs(result.getTime() - expectedDate.getTime());
expect(timeDiff).toBeLessThan(1000); // Less than 1 second difference
});

it('should return a Date object', () => {
const result = createTempChatExpirationDate();
expect(result).toBeInstanceOf(Date);
});

it('should return a future date', () => {
const now = new Date();
const result = createTempChatExpirationDate();
expect(result.getTime()).toBeGreaterThan(now.getTime());
});
});
});
84 changes: 84 additions & 0 deletions api/server/utils/tempChatRetention.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
const { logger } = require('~/config');

/**
* Default retention period for temporary chats in days
*/
const DEFAULT_RETENTION_DAYS = 30;

/**
* Minimum allowed retention period in days
*/
const MIN_RETENTION_DAYS = 1;

/**
* Maximum allowed retention period in days (1 year)
*/
const MAX_RETENTION_DAYS = 365;

/**
* Gets the temporary chat retention period from environment variables or config
* @param {TCustomConfig} [config] - The custom configuration object
* @returns {number} The retention period in days
*/
function getTempChatRetentionDays(config) {
let retentionDays = DEFAULT_RETENTION_DAYS;

// Check environment variable first
if (process.env.TEMP_CHAT_RETENTION_DAYS) {
const envValue = parseInt(process.env.TEMP_CHAT_RETENTION_DAYS, 10);
if (!isNaN(envValue)) {
retentionDays = envValue;
} else {
logger.warn(
`Invalid TEMP_CHAT_RETENTION_DAYS environment variable: ${process.env.TEMP_CHAT_RETENTION_DAYS}. Using default: ${DEFAULT_RETENTION_DAYS} days.`,
);
}
}

// Check config file (takes precedence over environment variable)
if (config?.interface?.temporaryChatRetentionDays !== undefined) {
const configValue = config.interface.temporaryChatRetentionDays;
if (typeof configValue === 'number' && !isNaN(configValue)) {
retentionDays = configValue;
} else {
logger.warn(
`Invalid temporaryChatRetentionDays in config: ${configValue}. Using ${retentionDays} days.`,
);
}
}

// Validate the retention period
if (retentionDays < MIN_RETENTION_DAYS) {
logger.warn(
`Temporary chat retention period ${retentionDays} is below minimum ${MIN_RETENTION_DAYS} days. Using minimum value.`,
);
retentionDays = MIN_RETENTION_DAYS;
} else if (retentionDays > MAX_RETENTION_DAYS) {
logger.warn(
`Temporary chat retention period ${retentionDays} exceeds maximum ${MAX_RETENTION_DAYS} days. Using maximum value.`,
);
retentionDays = MAX_RETENTION_DAYS;
}

return retentionDays;
}

/**
* Creates an expiration date for temporary chats
* @param {TCustomConfig} [config] - The custom configuration object
* @returns {Date} The expiration date
*/
function createTempChatExpirationDate(config) {
const retentionDays = getTempChatRetentionDays(config);
const expiredAt = new Date();
expiredAt.setDate(expiredAt.getDate() + retentionDays);
return expiredAt;
}

module.exports = {
DEFAULT_RETENTION_DAYS,
MIN_RETENTION_DAYS,
MAX_RETENTION_DAYS,
getTempChatRetentionDays,
createTempChatExpirationDate,
};
2 changes: 2 additions & 0 deletions librechat.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ interface:
bookmarks: true
multiConvo: true
agents: true
# Temporary chat retention period in days (default: 30, min: 1, max: 365)
# temporaryChatRetentionDays: 30

# Example Cloudflare turnstile (optional)
#turnstile:
Expand Down
11 changes: 6 additions & 5 deletions packages/data-provider/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { z } from 'zod';
import type { ZodError } from 'zod';
import type { TModelsConfig } from './types';
import { EModelEndpoint, eModelEndpointSchema } from './schemas';
import { specsConfigSchema, TSpecsConfig } from './models';
import { z } from 'zod';
import { fileConfigSchema } from './file-config';
import { FileSources } from './types/files';
import { MCPServersSchema } from './mcp';
import { specsConfigSchema, TSpecsConfig } from './models';
import { EModelEndpoint, eModelEndpointSchema } from './schemas';
import type { TModelsConfig } from './types';
import { FileSources } from './types/files';

export const defaultSocialLogins = ['google', 'facebook', 'openid', 'github', 'discord', 'saml'];

Expand Down Expand Up @@ -503,6 +503,7 @@ export const intefaceSchema = z
prompts: z.boolean().optional(),
agents: z.boolean().optional(),
temporaryChat: z.boolean().optional(),
temporaryChatRetentionDays: z.number().min(1).max(365).optional(),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

documentation update needed: availableTools

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, nitpick: rename to temporaryChatRetention and make unit to hours for more granular customization

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name update is done and documentation added here.

runCode: z.boolean().optional(),
webSearch: z.boolean().optional(),
})
Expand Down