Skip to content

Commit 5694ad4

Browse files
authored
🧠 feat: Prompt caching switch, prompt query params; refactor: static cache, prompt/markdown styling, trim copied code, switch new chat to convo URL (danny-avila#3784)
* refactor: Update staticCache to use oneDayInSeconds for sMaxAge and maxAge * refactor: role updates * style: first pass cursor * style: Update nested list styles in style.css * feat: setIsSubmitting to true in message handler to prevent edge case where submitting turns false during message stream * feat: Add logic to redirect to conversation page after creating a new conversation * refactor: Trim code string before copying in CodeBlock component * feat: configSchema bookmarks and presets defaults * feat: Update loadDefaultInterface to handle undefined config * refactor: use for compression check * feat: first pass, query params * fix: styling issues for prompt cards * feat: anthropic prompt caching UI switch * chore: Update static file cache control defaults/comments in .env.example * ci: fix tests * ci: fix tests * chore: use "submitting" class server error connection suspense fallback
1 parent bd701c1 commit 5694ad4

File tree

31 files changed

+513
-106
lines changed

31 files changed

+513
-106
lines changed

‎.env.example‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -431,10 +431,10 @@ ALLOW_SHARED_LINKS_PUBLIC=true
431431
# Static File Cache Control #
432432
#==============================#
433433

434-
# Leave commented out to use default of 1 month for max-age and 1 week for s-maxage
434+
# Leave commented out to use defaults: 1 day (86400 seconds) for s-maxage and 2 days (172800 seconds) for max-age
435435
# NODE_ENV must be set to production for these to take effect
436-
# STATIC_CACHE_MAX_AGE=604800
437-
# STATIC_CACHE_S_MAX_AGE=259200
436+
# STATIC_CACHE_MAX_AGE=172800
437+
# STATIC_CACHE_S_MAX_AGE=86400
438438

439439
# If you have another service in front of your LibreChat doing compression, disable express based compression here
440440
# DISABLE_COMPRESSION=true

‎api/app/clients/AnthropicClient.js‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,8 @@ class AnthropicClient extends BaseClient {
9494
const modelMatch = matchModelName(this.modelOptions.model, EModelEndpoint.anthropic);
9595
this.isClaude3 = modelMatch.startsWith('claude-3');
9696
this.isLegacyOutput = !modelMatch.startsWith('claude-3-5-sonnet');
97-
this.supportsCacheControl = this.checkPromptCacheSupport(modelMatch);
97+
this.supportsCacheControl =
98+
this.options.promptCache && this.checkPromptCacheSupport(modelMatch);
9899

99100
if (
100101
this.isLegacyOutput &&
@@ -821,6 +822,7 @@ class AnthropicClient extends BaseClient {
821822
maxContextTokens: this.options.maxContextTokens,
822823
promptPrefix: this.options.promptPrefix,
823824
modelLabel: this.options.modelLabel,
825+
promptCache: this.options.promptCache,
824826
resendFiles: this.options.resendFiles,
825827
iconURL: this.options.iconURL,
826828
greeting: this.options.greeting,

‎api/app/clients/specs/AnthropicClient.test.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ describe('AnthropicClient', () => {
206206
const modelOptions = {
207207
model: 'claude-3-5-sonnet-20240307',
208208
};
209-
client.setOptions({ modelOptions });
209+
client.setOptions({ modelOptions, promptCache: true });
210210
const anthropicClient = client.getClient(modelOptions);
211211
expect(anthropicClient._options.defaultHeaders).toBeDefined();
212212
expect(anthropicClient._options.defaultHeaders).toHaveProperty('anthropic-beta');
@@ -220,7 +220,7 @@ describe('AnthropicClient', () => {
220220
const modelOptions = {
221221
model: 'claude-3-haiku-2028',
222222
};
223-
client.setOptions({ modelOptions });
223+
client.setOptions({ modelOptions, promptCache: true });
224224
const anthropicClient = client.getClient(modelOptions);
225225
expect(anthropicClient._options.defaultHeaders).toBeDefined();
226226
expect(anthropicClient._options.defaultHeaders).toHaveProperty('anthropic-beta');

‎api/models/Role.js‎

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -76,46 +76,57 @@ const permissionSchemas = {
7676
};
7777

7878
/**
79-
* Updates access permissions for a specific role and permission type.
79+
* Updates access permissions for a specific role and multiple permission types.
8080
* @param {SystemRoles} roleName - The role to update.
81-
* @param {PermissionTypes} permissionType - The type of permission to update.
82-
* @param {Object.<Permissions, boolean>} permissions - Permissions to update and their values.
81+
* @param {Object.<PermissionTypes, Object.<Permissions, boolean>>} permissionsUpdate - Permissions to update and their values.
8382
*/
84-
async function updateAccessPermissions(roleName, permissionType, _permissions) {
85-
const permissions = removeNullishValues(_permissions);
86-
if (Object.keys(permissions).length === 0) {
83+
async function updateAccessPermissions(roleName, permissionsUpdate) {
84+
const updates = {};
85+
for (const [permissionType, permissions] of Object.entries(permissionsUpdate)) {
86+
if (permissionSchemas[permissionType]) {
87+
updates[permissionType] = removeNullishValues(permissions);
88+
}
89+
}
90+
91+
if (Object.keys(updates).length === 0) {
8792
return;
8893
}
8994

9095
try {
9196
const role = await getRoleByName(roleName);
92-
if (!role || !permissionSchemas[permissionType]) {
97+
if (!role) {
9398
return;
9499
}
95100

96-
await updateRoleByName(roleName, {
97-
[permissionType]: {
98-
...role[permissionType],
99-
...permissionSchemas[permissionType].partial().parse(permissions),
100-
},
101-
});
101+
const updatedPermissions = {};
102+
let hasChanges = false;
103+
104+
for (const [permissionType, permissions] of Object.entries(updates)) {
105+
const currentPermissions = role[permissionType] || {};
106+
updatedPermissions[permissionType] = { ...currentPermissions };
102107

103-
Object.entries(permissions).forEach(([permission, value]) =>
104-
logger.info(
105-
`Updated '${roleName}' role ${permissionType} '${permission}' permission to: ${value}`,
106-
),
107-
);
108+
for (const [permission, value] of Object.entries(permissions)) {
109+
if (currentPermissions[permission] !== value) {
110+
updatedPermissions[permissionType][permission] = value;
111+
hasChanges = true;
112+
logger.info(
113+
`Updating '${roleName}' role ${permissionType} '${permission}' permission from ${currentPermissions[permission]} to: ${value}`,
114+
);
115+
}
116+
}
117+
}
118+
119+
if (hasChanges) {
120+
await updateRoleByName(roleName, updatedPermissions);
121+
logger.info(`Updated '${roleName}' role permissions`);
122+
} else {
123+
logger.info(`No changes needed for '${roleName}' role permissions`);
124+
}
108125
} catch (error) {
109-
logger.error(`Failed to update ${roleName} role ${permissionType} permissions:`, error);
126+
logger.error(`Failed to update ${roleName} role permissions:`, error);
110127
}
111128
}
112129

113-
const updatePromptsAccess = (roleName, permissions) =>
114-
updateAccessPermissions(roleName, PermissionTypes.PROMPTS, permissions);
115-
116-
const updateBookmarksAccess = (roleName, permissions) =>
117-
updateAccessPermissions(roleName, PermissionTypes.BOOKMARKS, permissions);
118-
119130
/**
120131
* Initialize default roles in the system.
121132
* Creates the default roles (ADMIN, USER) if they don't exist in the database.
@@ -138,6 +149,5 @@ module.exports = {
138149
getRoleByName,
139150
initializeRoles,
140151
updateRoleByName,
141-
updatePromptsAccess,
142-
updateBookmarksAccess,
152+
updateAccessPermissions,
143153
};

‎api/models/Role.spec.js‎

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
const mongoose = require('mongoose');
2+
const { MongoMemoryServer } = require('mongodb-memory-server');
3+
const { SystemRoles, PermissionTypes } = require('librechat-data-provider');
4+
const Role = require('~/models/schema/roleSchema');
5+
const { updateAccessPermissions } = require('~/models/Role');
6+
const getLogStores = require('~/cache/getLogStores');
7+
8+
// Mock the cache
9+
jest.mock('~/cache/getLogStores', () => {
10+
return jest.fn().mockReturnValue({
11+
get: jest.fn(),
12+
set: jest.fn(),
13+
del: jest.fn(),
14+
});
15+
});
16+
17+
let mongoServer;
18+
19+
beforeAll(async () => {
20+
mongoServer = await MongoMemoryServer.create();
21+
const mongoUri = mongoServer.getUri();
22+
await mongoose.connect(mongoUri);
23+
});
24+
25+
afterAll(async () => {
26+
await mongoose.disconnect();
27+
await mongoServer.stop();
28+
});
29+
30+
beforeEach(async () => {
31+
await Role.deleteMany({});
32+
getLogStores.mockClear();
33+
});
34+
35+
describe('updateAccessPermissions', () => {
36+
it('should update permissions when changes are needed', async () => {
37+
await new Role({
38+
name: SystemRoles.USER,
39+
[PermissionTypes.PROMPTS]: {
40+
CREATE: true,
41+
USE: true,
42+
SHARED_GLOBAL: false,
43+
},
44+
}).save();
45+
46+
await updateAccessPermissions(SystemRoles.USER, {
47+
[PermissionTypes.PROMPTS]: {
48+
CREATE: true,
49+
USE: true,
50+
SHARED_GLOBAL: true,
51+
},
52+
});
53+
54+
const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean();
55+
expect(updatedRole[PermissionTypes.PROMPTS]).toEqual({
56+
CREATE: true,
57+
USE: true,
58+
SHARED_GLOBAL: true,
59+
});
60+
});
61+
62+
it('should not update permissions when no changes are needed', async () => {
63+
await new Role({
64+
name: SystemRoles.USER,
65+
[PermissionTypes.PROMPTS]: {
66+
CREATE: true,
67+
USE: true,
68+
SHARED_GLOBAL: false,
69+
},
70+
}).save();
71+
72+
await updateAccessPermissions(SystemRoles.USER, {
73+
[PermissionTypes.PROMPTS]: {
74+
CREATE: true,
75+
USE: true,
76+
SHARED_GLOBAL: false,
77+
},
78+
});
79+
80+
const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean();
81+
expect(updatedRole[PermissionTypes.PROMPTS]).toEqual({
82+
CREATE: true,
83+
USE: true,
84+
SHARED_GLOBAL: false,
85+
});
86+
});
87+
88+
it('should handle non-existent roles', async () => {
89+
await updateAccessPermissions('NON_EXISTENT_ROLE', {
90+
[PermissionTypes.PROMPTS]: {
91+
CREATE: true,
92+
},
93+
});
94+
95+
const role = await Role.findOne({ name: 'NON_EXISTENT_ROLE' });
96+
expect(role).toBeNull();
97+
});
98+
99+
it('should update only specified permissions', async () => {
100+
await new Role({
101+
name: SystemRoles.USER,
102+
[PermissionTypes.PROMPTS]: {
103+
CREATE: true,
104+
USE: true,
105+
SHARED_GLOBAL: false,
106+
},
107+
}).save();
108+
109+
await updateAccessPermissions(SystemRoles.USER, {
110+
[PermissionTypes.PROMPTS]: {
111+
SHARED_GLOBAL: true,
112+
},
113+
});
114+
115+
const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean();
116+
expect(updatedRole[PermissionTypes.PROMPTS]).toEqual({
117+
CREATE: true,
118+
USE: true,
119+
SHARED_GLOBAL: true,
120+
});
121+
});
122+
123+
it('should handle partial updates', async () => {
124+
await new Role({
125+
name: SystemRoles.USER,
126+
[PermissionTypes.PROMPTS]: {
127+
CREATE: true,
128+
USE: true,
129+
SHARED_GLOBAL: false,
130+
},
131+
}).save();
132+
133+
await updateAccessPermissions(SystemRoles.USER, {
134+
[PermissionTypes.PROMPTS]: {
135+
USE: false,
136+
},
137+
});
138+
139+
const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean();
140+
expect(updatedRole[PermissionTypes.PROMPTS]).toEqual({
141+
CREATE: true,
142+
USE: false,
143+
SHARED_GLOBAL: false,
144+
});
145+
});
146+
147+
it('should update multiple permission types at once', async () => {
148+
await new Role({
149+
name: SystemRoles.USER,
150+
[PermissionTypes.PROMPTS]: {
151+
CREATE: true,
152+
USE: true,
153+
SHARED_GLOBAL: false,
154+
},
155+
[PermissionTypes.BOOKMARKS]: {
156+
USE: true,
157+
},
158+
}).save();
159+
160+
await updateAccessPermissions(SystemRoles.USER, {
161+
[PermissionTypes.PROMPTS]: { USE: false, SHARED_GLOBAL: true },
162+
[PermissionTypes.BOOKMARKS]: { USE: false },
163+
});
164+
165+
const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean();
166+
expect(updatedRole[PermissionTypes.PROMPTS]).toEqual({
167+
CREATE: true,
168+
USE: false,
169+
SHARED_GLOBAL: true,
170+
});
171+
expect(updatedRole[PermissionTypes.BOOKMARKS]).toEqual({
172+
USE: false,
173+
});
174+
});
175+
176+
it('should handle updates for a single permission type', async () => {
177+
await new Role({
178+
name: SystemRoles.USER,
179+
[PermissionTypes.PROMPTS]: {
180+
CREATE: true,
181+
USE: true,
182+
SHARED_GLOBAL: false,
183+
},
184+
}).save();
185+
186+
await updateAccessPermissions(SystemRoles.USER, {
187+
[PermissionTypes.PROMPTS]: { USE: false, SHARED_GLOBAL: true },
188+
});
189+
190+
const updatedRole = await Role.findOne({ name: SystemRoles.USER }).lean();
191+
expect(updatedRole[PermissionTypes.PROMPTS]).toEqual({
192+
CREATE: true,
193+
USE: false,
194+
SHARED_GLOBAL: true,
195+
});
196+
});
197+
});

‎api/models/schema/defaults.js‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ const conversationPreset = {
7474
resendImages: {
7575
type: Boolean,
7676
},
77+
/* Anthropic only */
78+
promptCache: {
79+
type: Boolean,
80+
},
7781
// files
7882
resendFiles: {
7983
type: Boolean,

‎api/server/index.js‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ const validateImageRequest = require('./middleware/validateImageRequest');
1616
const errorController = require('./controllers/ErrorController');
1717
const configureSocialLogins = require('./socialLogins');
1818
const AppService = require('./services/AppService');
19+
const staticCache = require('./utils/staticCache');
1920
const noIndex = require('./middleware/noIndex');
2021
const routes = require('./routes');
21-
const staticCache = require('./utils/staticCache');
2222

2323
const { PORT, HOST, ALLOW_SOCIAL_LOGIN, DISABLE_COMPRESSION } = process.env ?? {};
2424

@@ -51,7 +51,7 @@ const startServer = async () => {
5151
app.set('trust proxy', 1); /* trust first proxy */
5252
app.use(cors());
5353

54-
if (DISABLE_COMPRESSION !== 'true') {
54+
if (!isEnabled(DISABLE_COMPRESSION)) {
5555
app.use(compression());
5656
}
5757

0 commit comments

Comments
 (0)