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

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,4 +1,5 @@
const { z } = require('zod');
const { createTempChatExpirationDate } = require('@librechat/api');
const { logger } = require('@librechat/data-schemas');
const { Message } = require('~/db/models');

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 @@ -98,6 +98,7 @@ const AppService = async (app) => {
interfaceConfig,
turnstileConfig,
balance,
config,
mcpConfig,
};

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 @@ -164,6 +164,13 @@ describe('AppService', () => {
disableBuilder: false,
capabilities: expect.arrayContaining([...defaultAgentCapabilities]),
},
config: {
registration: { socialLogins: ['testLogin'] },
fileStrategy: 'testStrategy',
balance: {
enabled: true,
},
},
});
});

Expand Down
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 hours (default: 720, min: 1, max: 8760)
# temporaryChatRetention: 1

# Example Cloudflare turnstile (optional)
#turnstile:
Expand Down
1 change: 1 addition & 0 deletions packages/api/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ export * from './events';
export * from './files';
export * from './generators';
export * from './openid';
export * from './tempChatRetention';
export { default as Tokenizer } from './tokenizer';
133 changes: 133 additions & 0 deletions packages/api/src/utils/tempChatRetention.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import {
DEFAULT_RETENTION_HOURS,
MIN_RETENTION_HOURS,
MAX_RETENTION_HOURS,
getTempChatRetentionHours,
createTempChatExpirationDate,
} from './tempChatRetention';
import type { TCustomConfig } from 'librechat-data-provider';

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

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

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

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

it('should use environment variable when set', () => {
process.env.TEMP_CHAT_RETENTION_HOURS = '48';
const result = getTempChatRetentionHours();
expect(result).toBe(48);
});

it('should use config value when set', () => {
const config: TCustomConfig = {
interface: {
temporaryChatRetention: 12,
},
};
const result = getTempChatRetentionHours(config);
expect(result).toBe(12);
});

it('should prioritize config over environment variable', () => {
process.env.TEMP_CHAT_RETENTION_HOURS = '48';
const config: TCustomConfig = {
interface: {
temporaryChatRetention: 12,
},
};
const result = getTempChatRetentionHours(config);
expect(result).toBe(12);
});

it('should enforce minimum retention period', () => {
const config: TCustomConfig = {
interface: {
temporaryChatRetention: 0,
},
};
const result = getTempChatRetentionHours(config);
expect(result).toBe(MIN_RETENTION_HOURS);
});

it('should enforce maximum retention period', () => {
const config: TCustomConfig = {
interface: {
temporaryChatRetention: 10000,
},
};
const result = getTempChatRetentionHours(config);
expect(result).toBe(MAX_RETENTION_HOURS);
});

it('should handle invalid environment variable', () => {
process.env.TEMP_CHAT_RETENTION_HOURS = 'invalid';
const result = getTempChatRetentionHours();
expect(result).toBe(DEFAULT_RETENTION_HOURS);
});

it('should handle invalid config value', () => {
const config: TCustomConfig = {
interface: {
temporaryChatRetention: 'invalid' as any,
},
};
const result = getTempChatRetentionHours(config);
expect(result).toBe(DEFAULT_RETENTION_HOURS);
});
});

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

const expectedDate = new Date();
expectedDate.setHours(expectedDate.getHours() + DEFAULT_RETENTION_HOURS);

// 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: TCustomConfig = {
interface: {
temporaryChatRetention: 12,
},
};

const result = createTempChatExpirationDate(config);

const expectedDate = new Date();
expectedDate.setHours(expectedDate.getHours() + 12);

// 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());
});
});
});
77 changes: 77 additions & 0 deletions packages/api/src/utils/tempChatRetention.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { logger } from '@librechat/data-schemas';
import type { TCustomConfig } from 'librechat-data-provider';

/**
* Default retention period for temporary chats in hours
*/
export const DEFAULT_RETENTION_HOURS = 24 * 30; // 30 days

/**
* Minimum allowed retention period in hours
*/
export const MIN_RETENTION_HOURS = 1;

/**
* Maximum allowed retention period in hours (1 year = 8760 hours)
*/
export const MAX_RETENTION_HOURS = 8760;

/**
* Gets the temporary chat retention period from environment variables or config
* @param config - The custom configuration object
* @returns The retention period in hours
*/
export function getTempChatRetentionHours(config?: TCustomConfig): number {
let retentionHours = DEFAULT_RETENTION_HOURS;

// Check environment variable first
if (process.env.TEMP_CHAT_RETENTION_HOURS) {
const envValue = parseInt(process.env.TEMP_CHAT_RETENTION_HOURS, 10);
if (!isNaN(envValue)) {
retentionHours = envValue;
} else {
logger.warn(
`Invalid TEMP_CHAT_RETENTION_HOURS environment variable: ${process.env.TEMP_CHAT_RETENTION_HOURS}. Using default: ${DEFAULT_RETENTION_HOURS} hours.`,
);
}
}

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

// Validate the retention period
if (retentionHours < MIN_RETENTION_HOURS) {
logger.warn(
`Temporary chat retention period ${retentionHours} is below minimum ${MIN_RETENTION_HOURS} hours. Using minimum value.`,
);
retentionHours = MIN_RETENTION_HOURS;
} else if (retentionHours > MAX_RETENTION_HOURS) {
logger.warn(
`Temporary chat retention period ${retentionHours} exceeds maximum ${MAX_RETENTION_HOURS} hours. Using maximum value.`,
);
retentionHours = MAX_RETENTION_HOURS;
}

return retentionHours;
}

/**
* Creates an expiration date for temporary chats
* @param config - The custom configuration object
* @returns The expiration date
*/
export function createTempChatExpirationDate(config?: TCustomConfig): Date {
const retentionHours = getTempChatRetentionHours(config);
const expiredAt = new Date();
expiredAt.setHours(expiredAt.getHours() + retentionHours);
return expiredAt;
}
1 change: 1 addition & 0 deletions packages/data-provider/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,7 @@ export const intefaceSchema = z
prompts: z.boolean().optional(),
agents: z.boolean().optional(),
temporaryChat: z.boolean().optional(),
temporaryChatRetention: z.number().min(1).max(8760).optional(),
runCode: z.boolean().optional(),
webSearch: z.boolean().optional(),
})
Expand Down