Merge branch 'dev' into feat/prompt-enhancement

This commit is contained in:
Marco Beretta 2025-06-23 14:48:46 +02:00 committed by GitHub
commit 3d261a969d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
365 changed files with 23826 additions and 8790 deletions

View file

@ -1,6 +1,6 @@
{
"name": "librechat-data-provider",
"version": "0.7.86",
"version": "0.7.88",
"description": "data services for librechat apps",
"main": "dist/index.js",
"module": "dist/index.es.js",

View file

@ -1,6 +1,8 @@
import axios from 'axios';
import { z } from 'zod';
import { OpenAPIV3 } from 'openapi-types';
import axios from 'axios';
import type { OpenAPIV3 } from 'openapi-types';
import type { ParametersSchema } from '../src/actions';
import type { FlowchartSchema } from './openapiSpecs';
import {
createURL,
resolveRef,
@ -15,9 +17,7 @@ import {
scholarAIOpenapiSpec,
swapidev,
} from './openapiSpecs';
import { AuthorizationTypeEnum, AuthTypeEnum } from '../src/types/assistants';
import type { FlowchartSchema } from './openapiSpecs';
import type { ParametersSchema } from '../src/actions';
import { AuthorizationTypeEnum, AuthTypeEnum } from '../src/types/agents';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;

View file

@ -1,4 +1,28 @@
import { StdioOptionsSchema, StreamableHTTPOptionsSchema, processMCPEnv, MCPOptions } from '../src/mcp';
import type { TUser } from 'librechat-data-provider';
import {
StreamableHTTPOptionsSchema,
StdioOptionsSchema,
processMCPEnv,
MCPOptions,
} from '../src/mcp';
// Helper function to create test user objects
function createTestUser(
overrides: Partial<TUser> & Record<string, unknown> = {},
): TUser & Record<string, unknown> {
return {
id: 'test-user-id',
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').toISOString(),
updatedAt: new Date('2021-01-01').toISOString(),
...overrides,
};
}
describe('Environment Variable Extraction (MCP)', () => {
const originalEnv = process.env;
@ -91,13 +115,13 @@ describe('Environment Variable Extraction (MCP)', () => {
// Type is now required, so parsing should fail
expect(() => StreamableHTTPOptionsSchema.parse(options)).toThrow();
// With type provided, it should pass
const validOptions = {
type: 'streamable-http' as const,
url: 'https://example.com/api',
};
const result = StreamableHTTPOptionsSchema.parse(validOptions);
expect(result.type).toBe('streamable-http');
});
@ -113,7 +137,7 @@ describe('Environment Variable Extraction (MCP)', () => {
};
const result = StreamableHTTPOptionsSchema.parse(options);
expect(result.headers).toEqual(options.headers);
});
});
@ -165,7 +189,7 @@ describe('Environment Variable Extraction (MCP)', () => {
});
it('should process user ID in headers field', () => {
const userId = 'test-user-123';
const user = createTestUser({ id: 'test-user-123' });
const obj: MCPOptions = {
type: 'sse',
url: 'https://example.com',
@ -176,7 +200,7 @@ describe('Environment Variable Extraction (MCP)', () => {
},
};
const result = processMCPEnv(obj, userId);
const result = processMCPEnv(obj, user);
expect('headers' in result && result.headers).toEqual({
Authorization: 'test-api-key-value',
@ -217,15 +241,15 @@ describe('Environment Variable Extraction (MCP)', () => {
};
// Process for two different users
const user1Id = 'user-123';
const user2Id = 'user-456';
const user1 = createTestUser({ id: 'user-123' });
const user2 = createTestUser({ id: 'user-456' });
const resultUser1 = processMCPEnv(baseConfig, user1Id);
const resultUser2 = processMCPEnv(baseConfig, user2Id);
const resultUser1 = processMCPEnv(baseConfig, user1);
const resultUser2 = processMCPEnv(baseConfig, user2);
// Verify each has the correct user ID
expect('headers' in resultUser1 && resultUser1.headers?.['User-Id']).toBe(user1Id);
expect('headers' in resultUser2 && resultUser2.headers?.['User-Id']).toBe(user2Id);
expect('headers' in resultUser1 && resultUser1.headers?.['User-Id']).toBe('user-123');
expect('headers' in resultUser2 && resultUser2.headers?.['User-Id']).toBe('user-456');
// Verify they're different objects
expect(resultUser1).not.toBe(resultUser2);
@ -239,11 +263,11 @@ describe('Environment Variable Extraction (MCP)', () => {
expect(baseConfig.headers?.['User-Id']).toBe('{{LIBRECHAT_USER_ID}}');
// Second user's config should be unchanged
expect('headers' in resultUser2 && resultUser2.headers?.['User-Id']).toBe(user2Id);
expect('headers' in resultUser2 && resultUser2.headers?.['User-Id']).toBe('user-456');
});
it('should process headers in streamable-http options', () => {
const userId = 'test-user-123';
const user = createTestUser({ id: 'test-user-123' });
const obj: MCPOptions = {
type: 'streamable-http',
url: 'https://example.com',
@ -254,7 +278,7 @@ describe('Environment Variable Extraction (MCP)', () => {
},
};
const result = processMCPEnv(obj, userId);
const result = processMCPEnv(obj, user);
expect('headers' in result && result.headers).toEqual({
Authorization: 'test-api-key-value',
@ -262,7 +286,7 @@ describe('Environment Variable Extraction (MCP)', () => {
'Content-Type': 'application/json',
});
});
it('should maintain streamable-http type in processed options', () => {
const obj: MCPOptions = {
type: 'streamable-http',
@ -273,5 +297,416 @@ describe('Environment Variable Extraction (MCP)', () => {
expect(result.type).toBe('streamable-http');
});
it('should process dynamic user fields in headers', () => {
const user = createTestUser({
id: 'user-123',
email: 'test@example.com',
username: 'testuser',
openidId: 'openid-123',
googleId: 'google-456',
emailVerified: true,
role: 'admin',
});
const obj: MCPOptions = {
type: 'sse',
url: 'https://example.com',
headers: {
'User-Email': '{{LIBRECHAT_USER_EMAIL}}',
'User-Name': '{{LIBRECHAT_USER_USERNAME}}',
OpenID: '{{LIBRECHAT_USER_OPENIDID}}',
'Google-ID': '{{LIBRECHAT_USER_GOOGLEID}}',
'Email-Verified': '{{LIBRECHAT_USER_EMAILVERIFIED}}',
'User-Role': '{{LIBRECHAT_USER_ROLE}}',
'Content-Type': 'application/json',
},
};
const result = processMCPEnv(obj, user);
expect('headers' in result && result.headers).toEqual({
'User-Email': 'test@example.com',
'User-Name': 'testuser',
OpenID: 'openid-123',
'Google-ID': 'google-456',
'Email-Verified': 'true',
'User-Role': 'admin',
'Content-Type': 'application/json',
});
});
it('should handle missing user fields gracefully', () => {
const user = createTestUser({
id: 'user-123',
email: 'test@example.com',
username: undefined, // explicitly set to undefined to test missing field
});
const obj: MCPOptions = {
type: 'sse',
url: 'https://example.com',
headers: {
'User-Email': '{{LIBRECHAT_USER_EMAIL}}',
'User-Name': '{{LIBRECHAT_USER_USERNAME}}',
'Content-Type': 'application/json',
},
};
const result = processMCPEnv(obj, user);
expect('headers' in result && result.headers).toEqual({
'User-Email': 'test@example.com',
'User-Name': '', // Empty string for missing field
'Content-Type': 'application/json',
});
});
it('should process user fields in env variables', () => {
const user = createTestUser({
id: 'user-123',
email: 'test@example.com',
ldapId: 'ldap-user-123',
});
const obj: MCPOptions = {
command: 'node',
args: ['server.js'],
env: {
USER_EMAIL: '{{LIBRECHAT_USER_EMAIL}}',
LDAP_ID: '{{LIBRECHAT_USER_LDAPID}}',
API_KEY: '${TEST_API_KEY}',
},
};
const result = processMCPEnv(obj, user);
expect('env' in result && result.env).toEqual({
USER_EMAIL: 'test@example.com',
LDAP_ID: 'ldap-user-123',
API_KEY: 'test-api-key-value',
});
});
it('should process user fields in URL', () => {
const user = createTestUser({
id: 'user-123',
username: 'testuser',
});
const obj: MCPOptions = {
type: 'sse',
url: 'https://example.com/api/{{LIBRECHAT_USER_USERNAME}}/stream',
};
const result = processMCPEnv(obj, user);
expect('url' in result && result.url).toBe('https://example.com/api/testuser/stream');
});
it('should handle boolean user fields', () => {
const user = createTestUser({
id: 'user-123',
emailVerified: true,
twoFactorEnabled: false,
termsAccepted: true,
});
const obj: MCPOptions = {
type: 'sse',
url: 'https://example.com',
headers: {
'Email-Verified': '{{LIBRECHAT_USER_EMAILVERIFIED}}',
'Two-Factor': '{{LIBRECHAT_USER_TWOFACTORENABLED}}',
'Terms-Accepted': '{{LIBRECHAT_USER_TERMSACCEPTED}}',
},
};
const result = processMCPEnv(obj, user);
expect('headers' in result && result.headers).toEqual({
'Email-Verified': 'true',
'Two-Factor': 'false',
'Terms-Accepted': 'true',
});
});
it('should not process sensitive fields like password', () => {
const user = createTestUser({
id: 'user-123',
email: 'test@example.com',
password: 'secret-password',
});
const obj: MCPOptions = {
type: 'sse',
url: 'https://example.com',
headers: {
'User-Email': '{{LIBRECHAT_USER_EMAIL}}',
'User-Password': '{{LIBRECHAT_USER_PASSWORD}}', // This should not be processed
},
};
const result = processMCPEnv(obj, user);
expect('headers' in result && result.headers).toEqual({
'User-Email': 'test@example.com',
'User-Password': '{{LIBRECHAT_USER_PASSWORD}}', // Unchanged
});
});
it('should handle multiple occurrences of the same placeholder', () => {
const user = createTestUser({
id: 'user-123',
email: 'test@example.com',
});
const obj: MCPOptions = {
type: 'sse',
url: 'https://example.com',
headers: {
'Primary-Email': '{{LIBRECHAT_USER_EMAIL}}',
'Secondary-Email': '{{LIBRECHAT_USER_EMAIL}}',
'Backup-Email': '{{LIBRECHAT_USER_EMAIL}}',
},
};
const result = processMCPEnv(obj, user);
expect('headers' in result && result.headers).toEqual({
'Primary-Email': 'test@example.com',
'Secondary-Email': 'test@example.com',
'Backup-Email': 'test@example.com',
});
});
it('should support both id and _id properties for LIBRECHAT_USER_ID', () => {
// Test with 'id' property
const userWithId = createTestUser({
id: 'user-123',
email: 'test@example.com',
});
const obj1: MCPOptions = {
type: 'sse',
url: 'https://example.com',
headers: {
'User-Id': '{{LIBRECHAT_USER_ID}}',
},
};
const result1 = processMCPEnv(obj1, userWithId);
expect('headers' in result1 && result1.headers?.['User-Id']).toBe('user-123');
// Test with '_id' property only (should not work since we only check 'id')
const userWithUnderscore = createTestUser({
id: undefined, // Remove default id to test _id
_id: 'user-456',
email: 'test@example.com',
});
const obj2: MCPOptions = {
type: 'sse',
url: 'https://example.com',
headers: {
'User-Id': '{{LIBRECHAT_USER_ID}}',
},
};
const result2 = processMCPEnv(obj2, userWithUnderscore);
// Since we don't check _id, the placeholder should remain unchanged
expect('headers' in result2 && result2.headers?.['User-Id']).toBe('{{LIBRECHAT_USER_ID}}');
// Test with both properties (id takes precedence)
const userWithBoth = createTestUser({
id: 'user-789',
_id: 'user-000',
email: 'test@example.com',
});
const obj3: MCPOptions = {
type: 'sse',
url: 'https://example.com',
headers: {
'User-Id': '{{LIBRECHAT_USER_ID}}',
},
};
const result3 = processMCPEnv(obj3, userWithBoth);
expect('headers' in result3 && result3.headers?.['User-Id']).toBe('user-789');
});
it('should process customUserVars in env field', () => {
const user = createTestUser();
const customUserVars = {
CUSTOM_VAR_1: 'custom-value-1',
CUSTOM_VAR_2: 'custom-value-2',
};
const obj: MCPOptions = {
command: 'node',
args: ['server.js'],
env: {
VAR_A: '{{CUSTOM_VAR_1}}',
VAR_B: 'Value with {{CUSTOM_VAR_2}}',
VAR_C: '${TEST_API_KEY}',
VAR_D: '{{LIBRECHAT_USER_EMAIL}}',
},
};
const result = processMCPEnv(obj, user, customUserVars);
expect('env' in result && result.env).toEqual({
VAR_A: 'custom-value-1',
VAR_B: 'Value with custom-value-2',
VAR_C: 'test-api-key-value',
VAR_D: 'test@example.com',
});
});
it('should process customUserVars in headers field', () => {
const user = createTestUser();
const customUserVars = {
USER_TOKEN: 'user-specific-token',
REGION: 'us-west-1',
};
const obj: MCPOptions = {
type: 'sse',
url: 'https://example.com/api',
headers: {
Authorization: 'Bearer {{USER_TOKEN}}',
'X-Region': '{{REGION}}',
'X-System-Key': '${TEST_API_KEY}',
'X-User-Id': '{{LIBRECHAT_USER_ID}}',
},
};
const result = processMCPEnv(obj, user, customUserVars);
expect('headers' in result && result.headers).toEqual({
Authorization: 'Bearer user-specific-token',
'X-Region': 'us-west-1',
'X-System-Key': 'test-api-key-value',
'X-User-Id': 'test-user-id',
});
});
it('should process customUserVars in URL field', () => {
const user = createTestUser();
const customUserVars = {
API_VERSION: 'v2',
TENANT_ID: 'tenant123',
};
const obj: MCPOptions = {
type: 'websocket',
url: 'wss://example.com/{{TENANT_ID}}/api/{{API_VERSION}}?user={{LIBRECHAT_USER_ID}}&key=${TEST_API_KEY}',
};
const result = processMCPEnv(obj, user, customUserVars);
expect('url' in result && result.url).toBe(
'wss://example.com/tenant123/api/v2?user=test-user-id&key=test-api-key-value',
);
});
it('should prioritize customUserVars over user fields and system env vars if placeholders are the same (though not recommended)', () => {
// This tests the order of operations: customUserVars -> userFields -> systemEnv
// BUt it's generally not recommended to have overlapping placeholder names.
process.env.LIBRECHAT_USER_EMAIL = 'system-email-should-be-overridden';
const user = createTestUser({ email: 'user-email-should-be-overridden' });
const customUserVars = {
LIBRECHAT_USER_EMAIL: 'custom-email-wins',
};
const obj: MCPOptions = {
type: 'sse',
url: 'https://example.com/api',
headers: {
'Test-Email': '{{LIBRECHAT_USER_EMAIL}}', // Placeholder that could match custom, user, or system
},
};
const result = processMCPEnv(obj, user, customUserVars);
expect('headers' in result && result.headers?.['Test-Email']).toBe('custom-email-wins');
// Clean up env var
delete process.env.LIBRECHAT_USER_EMAIL;
});
it('should handle customUserVars with no matching placeholders', () => {
const user = createTestUser();
const customUserVars = {
UNUSED_VAR: 'unused-value',
};
const obj: MCPOptions = {
command: 'node',
args: ['server.js'],
env: {
API_KEY: '${TEST_API_KEY}',
},
};
const result = processMCPEnv(obj, user, customUserVars);
expect('env' in result && result.env).toEqual({
API_KEY: 'test-api-key-value',
});
});
it('should handle placeholders with no matching customUserVars (falling back to user/system vars)', () => {
const user = createTestUser({ email: 'user-provided-email@example.com' });
// No customUserVars provided or customUserVars is empty
const customUserVars = {};
const obj: MCPOptions = {
type: 'sse',
url: 'https://example.com/api',
headers: {
'User-Email-Header': '{{LIBRECHAT_USER_EMAIL}}', // Should use user.email
'System-Key-Header': '${TEST_API_KEY}', // Should use process.env.TEST_API_KEY
'Non-Existent-Custom': '{{NON_EXISTENT_CUSTOM_VAR}}', // Should remain as placeholder
},
};
const result = processMCPEnv(obj, user, customUserVars);
expect('headers' in result && result.headers).toEqual({
'User-Email-Header': 'user-provided-email@example.com',
'System-Key-Header': 'test-api-key-value',
'Non-Existent-Custom': '{{NON_EXISTENT_CUSTOM_VAR}}',
});
});
it('should correctly process a mix of all variable types', () => {
const user = createTestUser({ id: 'userXYZ', username: 'john.doe' });
const customUserVars = {
CUSTOM_ENDPOINT_ID: 'ep123',
ANOTHER_CUSTOM: 'another_val',
};
const obj = {
type: 'streamable-http' as const,
url: 'https://{{CUSTOM_ENDPOINT_ID}}.example.com/users/{{LIBRECHAT_USER_USERNAME}}',
headers: {
'X-Auth-Token': '{{CUSTOM_TOKEN_FROM_USER_SETTINGS}}', // Assuming this would be a custom var
'X-User-ID': '{{LIBRECHAT_USER_ID}}',
'X-System-Test-Key': '${TEST_API_KEY}', // Using existing env var from beforeEach
},
env: {
PROCESS_MODE: '{{PROCESS_MODE_CUSTOM}}', // Another custom var
USER_HOME_DIR: '/home/{{LIBRECHAT_USER_USERNAME}}',
SYSTEM_PATH: '${PATH}', // Example of a system env var
},
};
// Simulate customUserVars that would be passed, including those for headers and env
const allCustomVarsForCall = {
...customUserVars,
CUSTOM_TOKEN_FROM_USER_SETTINGS: 'secretToken123!',
PROCESS_MODE_CUSTOM: 'production',
};
// Cast obj to MCPOptions when calling processMCPEnv.
// This acknowledges the object might not strictly conform to one schema in the union,
// but we are testing the function's ability to handle these properties if present.
const result = processMCPEnv(obj as MCPOptions, user, allCustomVarsForCall);
expect('url' in result && result.url).toBe('https://ep123.example.com/users/john.doe');
expect('headers' in result && result.headers).toEqual({
'X-Auth-Token': 'secretToken123!',
'X-User-ID': 'userXYZ',
'X-System-Test-Key': 'test-api-key-value', // Expecting value of TEST_API_KEY
});
expect('env' in result && result.env).toEqual({
PROCESS_MODE: 'production',
USER_HOME_DIR: '/home/john.doe',
SYSTEM_PATH: process.env.PATH, // Actual value of PATH from the test environment
});
});
});
});

View file

@ -3,15 +3,11 @@ import _axios from 'axios';
import { URL } from 'url';
import crypto from 'crypto';
import { load } from 'js-yaml';
import type {
FunctionTool,
Schema,
Reference,
ActionMetadata,
ActionMetadataRuntime,
} from './types/assistants';
import type { ActionMetadata, ActionMetadataRuntime } from './types/agents';
import type { FunctionTool, Schema, Reference } from './types/assistants';
import { AuthTypeEnum, AuthorizationTypeEnum } from './types/agents';
import type { OpenAPIV3 } from 'openapi-types';
import { Tools, AuthTypeEnum, AuthorizationTypeEnum } from './types/assistants';
import { Tools } from './types/assistants';
export type ParametersSchema = {
type: string;

View file

@ -261,6 +261,7 @@ export const getUserPromptPreferences = () => `${prompts()}/preferences`;
export const roles = () => '/api/roles';
export const getRole = (roleName: string) => `${roles()}/${roleName.toLowerCase()}`;
export const updatePromptPermissions = (roleName: string) => `${getRole(roleName)}/prompts`;
export const updateMemoryPermissions = (roleName: string) => `${getRole(roleName)}/memories`;
export const updateAgentPermissions = (roleName: string) => `${getRole(roleName)}/agents`;
/* Conversation Tags */
@ -290,3 +291,8 @@ export const confirmTwoFactor = () => '/api/auth/2fa/confirm';
export const disableTwoFactor = () => '/api/auth/2fa/disable';
export const regenerateBackupCodes = () => '/api/auth/2fa/backup/regenerate';
export const verifyTwoFactorTemp = () => '/api/auth/2fa/verify-temp';
/* Memories */
export const memories = () => '/api/memories';
export const memory = (key: string) => `${memories()}/${encodeURIComponent(key)}`;
export const memoryPreferences = () => `${memories()}/preferences`;

View file

@ -244,21 +244,26 @@ export const defaultAgentCapabilities = [
AgentCapabilities.ocr,
];
export const agentsEndpointSChema = baseEndpointSchema.merge(
z.object({
/* agents specific */
recursionLimit: z.number().optional(),
disableBuilder: z.boolean().optional(),
maxRecursionLimit: z.number().optional(),
allowedProviders: z.array(z.union([z.string(), eModelEndpointSchema])).optional(),
capabilities: z
.array(z.nativeEnum(AgentCapabilities))
.optional()
.default(defaultAgentCapabilities),
}),
);
export const agentsEndpointSchema = baseEndpointSchema
.merge(
z.object({
/* agents specific */
recursionLimit: z.number().optional(),
disableBuilder: z.boolean().optional().default(false),
maxRecursionLimit: z.number().optional(),
allowedProviders: z.array(z.union([z.string(), eModelEndpointSchema])).optional(),
capabilities: z
.array(z.nativeEnum(AgentCapabilities))
.optional()
.default(defaultAgentCapabilities),
}),
)
.default({
disableBuilder: false,
capabilities: defaultAgentCapabilities,
});
export type TAgentsEndpoint = z.infer<typeof agentsEndpointSChema>;
export type TAgentsEndpoint = z.infer<typeof agentsEndpointSchema>;
export const endpointSchema = baseEndpointSchema.merge(
z.object({
@ -493,6 +498,7 @@ export const intefaceSchema = z
sidePanel: z.boolean().optional(),
multiConvo: z.boolean().optional(),
bookmarks: z.boolean().optional(),
memories: z.boolean().optional(),
presets: z.boolean().optional(),
prompts: z.boolean().optional(),
agents: z.boolean().optional(),
@ -508,6 +514,7 @@ export const intefaceSchema = z
presets: true,
multiConvo: true,
bookmarks: true,
memories: true,
prompts: true,
agents: true,
temporaryChat: true,
@ -581,11 +588,24 @@ export type TStartupConfig = {
scraperType?: ScraperTypes;
rerankerType?: RerankerTypes;
};
mcpServers?: Record<
string,
{
customUserVars: Record<
string,
{
title: string;
description: string;
}
>;
}
>;
};
export enum OCRStrategy {
MISTRAL_OCR = 'mistral_ocr',
CUSTOM_OCR = 'custom_ocr',
AZURE_MISTRAL_OCR = 'azure_mistral_ocr',
}
export enum SearchCategories {
@ -649,11 +669,35 @@ export const balanceSchema = z.object({
refillAmount: z.number().optional().default(10000),
});
export const memorySchema = z.object({
disabled: z.boolean().optional(),
validKeys: z.array(z.string()).optional(),
tokenLimit: z.number().optional(),
personalize: z.boolean().default(true),
messageWindowSize: z.number().optional().default(5),
agent: z
.union([
z.object({
id: z.string(),
}),
z.object({
provider: z.string(),
model: z.string(),
instructions: z.string().optional(),
model_parameters: z.record(z.any()).optional(),
}),
])
.optional(),
});
export type TMemoryConfig = z.infer<typeof memorySchema>;
export const configSchema = z.object({
version: z.string(),
cache: z.boolean().default(true),
ocr: ocrSchema.optional(),
webSearch: webSearchSchema.optional(),
memory: memorySchema.optional(),
secureImageLinks: z.boolean().optional(),
imageOutputType: z.nativeEnum(EImageOutputType).default(EImageOutputType.PNG),
includedTools: z.array(z.string()).optional(),
@ -694,7 +738,7 @@ export const configSchema = z.object({
[EModelEndpoint.azureOpenAI]: azureEndpointSchema.optional(),
[EModelEndpoint.azureAssistants]: assistantEndpointSchema.optional(),
[EModelEndpoint.assistants]: assistantEndpointSchema.optional(),
[EModelEndpoint.agents]: agentsEndpointSChema.optional(),
[EModelEndpoint.agents]: agentsEndpointSchema.optional(),
[EModelEndpoint.custom]: z.array(endpointSchema.partial()).optional(),
[EModelEndpoint.bedrock]: baseEndpointSchema.optional(),
})
@ -853,7 +897,6 @@ export const defaultModels = {
[EModelEndpoint.assistants]: [...sharedOpenAIModels, 'chatgpt-4o-latest'],
[EModelEndpoint.agents]: sharedOpenAIModels, // TODO: Add agent models (agentsModels)
[EModelEndpoint.google]: [
// Shared Google Models between Vertex AI & Gen AI
// Gemini 2.0 Models
'gemini-2.0-flash-001',
'gemini-2.0-flash-exp',
@ -1104,6 +1147,10 @@ export enum CacheKeys {
* Key for in-progress flow states.
*/
FLOWS = 'flows',
/**
* Key for individual MCP Tool Manifests.
*/
MCP_TOOLS = 'mcp_tools',
/**
* Key for pending chat requests (concurrency check)
*/
@ -1291,6 +1338,10 @@ export enum SettingsTabValues {
* Chat input commands
*/
COMMANDS = 'commands',
/**
* Tab for Personalization Settings
*/
PERSONALIZATION = 'personalization',
}
export enum STTProviders {
@ -1328,7 +1379,7 @@ export enum Constants {
/** Key for the app's version. */
VERSION = 'v0.7.8',
/** Key for the Custom Config's version (librechat.yaml). */
CONFIG_VERSION = '1.2.6',
CONFIG_VERSION = '1.2.8',
/** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */
NO_PARENT = '00000000-0000-0000-0000-000000000000',
/** Standard value for the initial conversationId before a request is sent */
@ -1355,6 +1406,8 @@ export enum Constants {
GLOBAL_PROJECT_NAME = 'instance',
/** Delimiter for MCP tools */
mcp_delimiter = '_mcp_',
/** Prefix for MCP plugins */
mcp_prefix = 'mcp_',
/** Placeholder Agent ID for Ephemeral Agents */
EPHEMERAL_AGENT_ID = 'ephemeral',
}
@ -1398,6 +1451,10 @@ export enum LocalStorageKeys {
LAST_CODE_TOGGLE_ = 'LAST_CODE_TOGGLE_',
/** Last checked toggle for Web Search per conversation ID */
LAST_WEB_SEARCH_TOGGLE_ = 'LAST_WEB_SEARCH_TOGGLE_',
/** Key for the last selected agent provider */
LAST_AGENT_PROVIDER = 'lastAgentProvider',
/** Key for the last selected agent model */
LAST_AGENT_MODEL = 'lastAgentModel',
}
export enum ForkOptions {

View file

@ -13,11 +13,11 @@ export default function createPayload(submission: t.TSubmission) {
ephemeralAgent,
} = submission;
const { conversationId } = s.tConvoUpdateSchema.parse(conversation);
const { endpoint, endpointType } = endpointOption as {
const { endpoint: _e, endpointType } = endpointOption as {
endpoint: s.EModelEndpoint;
endpointType?: s.EModelEndpoint;
};
const endpoint = _e as s.EModelEndpoint;
let server = EndpointURLs[endpointType ?? endpoint];
const isEphemeral = s.isEphemeralAgent(endpoint, ephemeralAgent);
@ -32,6 +32,7 @@ export default function createPayload(submission: t.TSubmission) {
const payload: t.TPayload = {
...userMessage,
...endpointOption,
endpoint,
ephemeralAgent: isEphemeral ? ephemeralAgent : undefined,
isContinued: !!(isEdited && isContinued),
conversationId,

View file

@ -2,6 +2,7 @@ import type { AxiosResponse } from 'axios';
import type * as t from './types';
import * as endpoints from './api-endpoints';
import * as a from './types/assistants';
import * as ag from './types/agents';
import * as m from './types/mutations';
import * as q from './types/queries';
import * as f from './types/files';
@ -150,7 +151,11 @@ export const updateUserPlugins = (payload: t.TUpdateUserPlugins) => {
/* Config */
export const getStartupConfig = (): Promise<config.TStartupConfig> => {
export const getStartupConfig = (): Promise<
config.TStartupConfig & {
mcpCustomUserVars?: Record<string, { title: string; description: string }>;
}
> => {
return request.get(endpoints.config());
};
@ -351,7 +356,7 @@ export const updateAction = (data: m.UpdateActionVariables): Promise<m.UpdateAct
);
};
export function getActions(): Promise<a.Action[]> {
export function getActions(): Promise<ag.Action[]> {
return request.get(
endpoints.agents({
path: 'actions',
@ -407,7 +412,7 @@ export const updateAgent = ({
export const duplicateAgent = ({
agent_id,
}: m.DuplicateAgentBody): Promise<{ agent: a.Agent; actions: a.Action[] }> => {
}: m.DuplicateAgentBody): Promise<{ agent: a.Agent; actions: ag.Action[] }> => {
return request.post(
endpoints.agents({
path: `${agent_id}/duplicate`,
@ -733,6 +738,12 @@ export function updateAgentPermissions(
return request.put(endpoints.updateAgentPermissions(variables.roleName), variables.updates);
}
export function updateMemoryPermissions(
variables: m.UpdateMemoryPermVars,
): Promise<m.UpdatePermResponse> {
return request.put(endpoints.updateMemoryPermissions(variables.roleName), variables.updates);
}
/* Tags */
export function getConversationTags(): Promise<t.TConversationTagsResponse> {
return request.get(endpoints.conversationTags());
@ -814,3 +825,33 @@ export function verifyTwoFactorTemp(
): Promise<t.TVerify2FATempResponse> {
return request.post(endpoints.verifyTwoFactorTemp(), payload);
}
/* Memories */
export const getMemories = (): Promise<q.MemoriesResponse> => {
return request.get(endpoints.memories());
};
export const deleteMemory = (key: string): Promise<void> => {
return request.delete(endpoints.memory(key));
};
export const updateMemory = (
key: string,
value: string,
originalKey?: string,
): Promise<q.TUserMemory> => {
return request.patch(endpoints.memory(originalKey || key), { key, value });
};
export const updateMemoryPreferences = (preferences: {
memories: boolean;
}): Promise<{ updated: boolean; preferences: { memories: boolean } }> => {
return request.patch(endpoints.memoryPreferences(), preferences);
};
export const createMemory = (data: {
key: string;
value: string;
}): Promise<{ created: boolean; memory: q.TUserMemory }> => {
return request.post(endpoints.memories(), data);
};

View file

@ -1,6 +1,6 @@
import { z } from 'zod';
import { EModelEndpoint } from './schemas';
import type { FileConfig, EndpointFileConfig } from './types/files';
import type { EndpointFileConfig, FileConfig } from './types/files';
export const supportsFiles = {
[EModelEndpoint.openAI]: true,
@ -49,6 +49,8 @@ export const fullMimeTypesList = [
'text/javascript',
'image/gif',
'image/png',
'image/heic',
'image/heif',
'application/x-tar',
'application/typescript',
'application/xml',
@ -80,6 +82,8 @@ export const codeInterpreterMimeTypesList = [
'text/javascript',
'image/gif',
'image/png',
'image/heic',
'image/heif',
'application/x-tar',
'application/typescript',
'application/xml',
@ -105,18 +109,18 @@ export const retrievalMimeTypesList = [
'text/plain',
];
export const imageExtRegex = /\.(jpg|jpeg|png|gif|webp)$/i;
export const imageExtRegex = /\.(jpg|jpeg|png|gif|webp|heic|heif)$/i;
export const excelMimeTypes =
/^application\/(vnd\.ms-excel|msexcel|x-msexcel|x-ms-excel|x-excel|x-dos_ms_excel|xls|x-xls|vnd\.openxmlformats-officedocument\.spreadsheetml\.sheet)$/;
export const textMimeTypes =
/^(text\/(x-c|x-csharp|tab-separated-values|x-c\+\+|x-java|html|markdown|x-php|x-python|x-script\.python|x-ruby|x-tex|plain|css|vtt|javascript|csv))$/;
/^(text\/(x-c|x-csharp|tab-separated-values|x-c\+\+|x-h|x-java|html|markdown|x-php|x-python|x-script\.python|x-ruby|x-tex|plain|css|vtt|javascript|csv))$/;
export const applicationMimeTypes =
/^(application\/(epub\+zip|csv|json|pdf|x-tar|typescript|vnd\.openxmlformats-officedocument\.(wordprocessingml\.document|presentationml\.presentation|spreadsheetml\.sheet)|xml|zip))$/;
export const imageMimeTypes = /^image\/(jpeg|gif|png|webp)$/;
export const imageMimeTypes = /^image\/(jpeg|gif|png|webp|heic|heif)$/;
export const supportedMimeTypes = [
textMimeTypes,
@ -138,6 +142,7 @@ export const codeTypeMapping: { [key: string]: string } = {
c: 'text/x-c',
cs: 'text/x-csharp',
cpp: 'text/x-c++',
h: 'text/x-h',
md: 'text/markdown',
php: 'text/x-php',
py: 'text/x-python',
@ -155,7 +160,7 @@ export const codeTypeMapping: { [key: string]: string } = {
};
export const retrievalMimeTypes = [
/^(text\/(x-c|x-c\+\+|html|x-java|markdown|x-php|x-python|x-script\.python|x-ruby|x-tex|plain|vtt|xml))$/,
/^(text\/(x-c|x-c\+\+|x-h|html|x-java|markdown|x-php|x-python|x-script\.python|x-ruby|x-tex|plain|vtt|xml))$/,
/^(application\/(json|pdf|vnd\.openxmlformats-officedocument\.(wordprocessingml\.document|presentationml\.presentation)))$/,
];

View file

@ -16,6 +16,8 @@ export * from './models';
export * from './mcp';
/* web search */
export * from './web';
/* memory */
export * from './memory';
/* RBAC */
export * from './permissions';
export * from './roles';

View file

@ -47,6 +47,8 @@ export enum QueryKeys {
health = 'health',
userTerms = 'userTerms',
banner = 'banner',
/* Memories */
memories = 'memories',
}
export enum MutationKeys {
@ -71,4 +73,5 @@ export enum MutationKeys {
updateRole = 'updateRole',
enableTwoFactor = 'enableTwoFactor',
verifyTwoFactor = 'verifyTwoFactor',
updateMemoryPreferences = 'updateMemoryPreferences',
}

View file

@ -1,5 +1,7 @@
import { z } from 'zod';
import type { TUser } from './types';
import { extractEnvVariable } from './utils';
import { TokenExchangeMethodEnum } from './types/agents';
const BaseOptionsSchema = z.object({
iconPath: z.string().optional(),
@ -7,6 +9,45 @@ const BaseOptionsSchema = z.object({
initTimeout: z.number().optional(),
/** Controls visibility in chat dropdown menu (MCPSelect) */
chatMenu: z.boolean().optional(),
/**
* Controls server instruction behavior:
* - undefined/not set: No instructions included (default)
* - true: Use server-provided instructions
* - string: Use custom instructions (overrides server-provided)
*/
serverInstructions: z.union([z.boolean(), z.string()]).optional(),
/**
* OAuth configuration for SSE and Streamable HTTP transports
* - Optional: OAuth can be auto-discovered on 401 responses
* - Pre-configured values will skip discovery steps
*/
oauth: z
.object({
/** OAuth authorization endpoint (optional - can be auto-discovered) */
authorization_url: z.string().url().optional(),
/** OAuth token endpoint (optional - can be auto-discovered) */
token_url: z.string().url().optional(),
/** OAuth client ID (optional - can use dynamic registration) */
client_id: z.string().optional(),
/** OAuth client secret (optional - can use dynamic registration) */
client_secret: z.string().optional(),
/** OAuth scopes to request */
scope: z.string().optional(),
/** OAuth redirect URI (defaults to /api/mcp/{serverName}/oauth/callback) */
redirect_uri: z.string().url().optional(),
/** Token exchange method */
token_exchange_method: z.nativeEnum(TokenExchangeMethodEnum).optional(),
})
.optional(),
customUserVars: z
.record(
z.string(),
z.object({
title: z.string(),
description: z.string(),
}),
)
.optional(),
});
export const StdioOptionsSchema = BaseOptionsSchema.extend({
@ -114,12 +155,100 @@ export const MCPServersSchema = z.record(z.string(), MCPOptionsSchema);
export type MCPOptions = z.infer<typeof MCPOptionsSchema>;
/**
* Recursively processes an object to replace environment variables in string values
* @param {MCPOptions} obj - The object to process
* @param {string} [userId] - The user ID
* @returns {MCPOptions} - The processed object with environment variables replaced
* List of allowed user fields that can be used in MCP environment variables.
* These are non-sensitive string/boolean fields from the IUser interface.
*/
export function processMCPEnv(obj: Readonly<MCPOptions>, userId?: string): MCPOptions {
const ALLOWED_USER_FIELDS = [
'name',
'username',
'email',
'provider',
'role',
'googleId',
'facebookId',
'openidId',
'samlId',
'ldapId',
'githubId',
'discordId',
'appleId',
'emailVerified',
'twoFactorEnabled',
'termsAccepted',
] as const;
/**
* Processes a string value to replace user field placeholders
* @param value - The string value to process
* @param user - The user object
* @returns The processed string with placeholders replaced
*/
function processUserPlaceholders(value: string, user?: TUser): string {
if (!user || typeof value !== 'string') {
return value;
}
for (const field of ALLOWED_USER_FIELDS) {
const placeholder = `{{LIBRECHAT_USER_${field.toUpperCase()}}}`;
if (value.includes(placeholder)) {
const fieldValue = user[field as keyof TUser];
const replacementValue = fieldValue != null ? String(fieldValue) : '';
value = value.replace(new RegExp(placeholder, 'g'), replacementValue);
}
}
return value;
}
function processSingleValue({
originalValue,
customUserVars,
user,
}: {
originalValue: string;
customUserVars?: Record<string, string>;
user?: TUser;
}): string {
let value = originalValue;
// 1. Replace custom user variables
if (customUserVars) {
for (const [varName, varVal] of Object.entries(customUserVars)) {
/** Escaped varName for use in regex to avoid issues with special characters */
const escapedVarName = varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const placeholderRegex = new RegExp(`\\{\\{${escapedVarName}\\}\\}`, 'g');
value = value.replace(placeholderRegex, varVal);
}
}
// 2.A. Special handling for LIBRECHAT_USER_ID placeholder
// This ensures {{LIBRECHAT_USER_ID}} is replaced only if user.id is available.
// If user.id is null/undefined, the placeholder remains
if (user && user.id != null && value.includes('{{LIBRECHAT_USER_ID}}')) {
value = value.replace(/\{\{LIBRECHAT_USER_ID\}\}/g, String(user.id));
}
// 2.B. Replace other standard user field placeholders (e.g., {{LIBRECHAT_USER_EMAIL}})
value = processUserPlaceholders(value, user);
// 3. Replace system environment variables
value = extractEnvVariable(value);
return value;
}
/**
* Recursively processes an object to replace environment variables in string values
* @param obj - The object to process
* @param user - The user object containing all user fields
* @param customUserVars - vars that user set in settings
* @returns - The processed object with environment variables replaced
*/
export function processMCPEnv(
obj: Readonly<MCPOptions>,
user?: TUser,
customUserVars?: Record<string, string>,
): MCPOptions {
if (obj === null || obj === undefined) {
return obj;
}
@ -128,24 +257,25 @@ export function processMCPEnv(obj: Readonly<MCPOptions>, userId?: string): MCPOp
if ('env' in newObj && newObj.env) {
const processedEnv: Record<string, string> = {};
for (const [key, value] of Object.entries(newObj.env)) {
processedEnv[key] = extractEnvVariable(value);
for (const [key, originalValue] of Object.entries(newObj.env)) {
processedEnv[key] = processSingleValue({ originalValue, customUserVars, user });
}
newObj.env = processedEnv;
} else if ('headers' in newObj && newObj.headers) {
}
// Process headers if they exist (for WebSocket, SSE, StreamableHTTP types)
// Note: `env` and `headers` are on different branches of the MCPOptions union type.
if ('headers' in newObj && newObj.headers) {
const processedHeaders: Record<string, string> = {};
for (const [key, value] of Object.entries(newObj.headers)) {
if (value === '{{LIBRECHAT_USER_ID}}' && userId != null && userId) {
processedHeaders[key] = userId;
continue;
}
processedHeaders[key] = extractEnvVariable(value);
for (const [key, originalValue] of Object.entries(newObj.headers)) {
processedHeaders[key] = processSingleValue({ originalValue, customUserVars, user });
}
newObj.headers = processedHeaders;
}
// Process URL if it exists (for WebSocket, SSE, StreamableHTTP types)
if ('url' in newObj && newObj.url) {
newObj.url = extractEnvVariable(newObj.url);
newObj.url = processSingleValue({ originalValue: newObj.url, customUserVars, user });
}
return newObj;

View file

@ -0,0 +1,62 @@
import type { TCustomConfig, TMemoryConfig } from './config';
/**
* Loads the memory configuration and validates it
* @param config - The memory configuration from librechat.yaml
* @returns The validated memory configuration
*/
export function loadMemoryConfig(config: TCustomConfig['memory']): TMemoryConfig | undefined {
if (!config) {
return undefined;
}
// If disabled is explicitly true, return the config as-is
if (config.disabled === true) {
return config;
}
// Check if the agent configuration is valid
const hasValidAgent =
config.agent &&
(('id' in config.agent && !!config.agent.id) ||
('provider' in config.agent &&
'model' in config.agent &&
!!config.agent.provider &&
!!config.agent.model));
// If agent config is invalid, treat as disabled
if (!hasValidAgent) {
return {
...config,
disabled: true,
};
}
return config;
}
/**
* Checks if memory feature is enabled based on the configuration
* @param config - The memory configuration
* @returns True if memory is enabled, false otherwise
*/
export function isMemoryEnabled(config: TMemoryConfig | undefined): boolean {
if (!config) {
return false;
}
if (config.disabled === true) {
return false;
}
// Check if agent configuration is valid
const hasValidAgent =
config.agent &&
(('id' in config.agent && !!config.agent.id) ||
('provider' in config.agent &&
'model' in config.agent &&
!!config.agent.provider &&
!!config.agent.model));
return !!hasValidAgent;
}

View file

@ -225,13 +225,15 @@ const extractOmniVersion = (modelStr: string): string => {
export const getResponseSender = (endpointOption: t.TEndpointOption): string => {
const {
model: _m,
endpoint,
endpoint: _e,
endpointType,
modelDisplayLabel: _mdl,
chatGptLabel: _cgl,
modelLabel: _ml,
} = endpointOption;
const endpoint = _e as EModelEndpoint;
const model = _m ?? '';
const modelDisplayLabel = _mdl ?? '';
const chatGptLabel = _cgl ?? '';

View file

@ -16,6 +16,10 @@ export enum PermissionTypes {
* Type for Agent Permissions
*/
AGENTS = 'AGENTS',
/**
* Type for Memory Permissions
*/
MEMORIES = 'MEMORIES',
/**
* Type for Multi-Conversation Permissions
*/
@ -45,6 +49,8 @@ export enum Permissions {
READ = 'READ',
READ_AUTHOR = 'READ_AUTHOR',
SHARE = 'SHARE',
/** Can disable if desired */
OPT_OUT = 'OPT_OUT',
}
export const promptPermissionsSchema = z.object({
@ -60,6 +66,15 @@ export const bookmarkPermissionsSchema = z.object({
});
export type TBookmarkPermissions = z.infer<typeof bookmarkPermissionsSchema>;
export const memoryPermissionsSchema = z.object({
[Permissions.USE]: z.boolean().default(true),
[Permissions.CREATE]: z.boolean().default(true),
[Permissions.UPDATE]: z.boolean().default(true),
[Permissions.READ]: z.boolean().default(true),
[Permissions.OPT_OUT]: z.boolean().default(true),
});
export type TMemoryPermissions = z.infer<typeof memoryPermissionsSchema>;
export const agentPermissionsSchema = z.object({
[Permissions.SHARED_GLOBAL]: z.boolean().default(false),
[Permissions.USE]: z.boolean().default(true),
@ -92,6 +107,7 @@ export type TWebSearchPermissions = z.infer<typeof webSearchPermissionsSchema>;
export const permissionsSchema = z.object({
[PermissionTypes.PROMPTS]: promptPermissionsSchema,
[PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema,
[PermissionTypes.MEMORIES]: memoryPermissionsSchema,
[PermissionTypes.AGENTS]: agentPermissionsSchema,
[PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema,
[PermissionTypes.TEMPORARY_CHAT]: temporaryChatPermissionsSchema,

View file

@ -5,6 +5,7 @@ import {
permissionsSchema,
agentPermissionsSchema,
promptPermissionsSchema,
memoryPermissionsSchema,
runCodePermissionsSchema,
webSearchPermissionsSchema,
bookmarkPermissionsSchema,
@ -48,6 +49,13 @@ const defaultRolesSchema = z.object({
[PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema.extend({
[Permissions.USE]: z.boolean().default(true),
}),
[PermissionTypes.MEMORIES]: memoryPermissionsSchema.extend({
[Permissions.USE]: z.boolean().default(true),
[Permissions.CREATE]: z.boolean().default(true),
[Permissions.UPDATE]: z.boolean().default(true),
[Permissions.READ]: z.boolean().default(true),
[Permissions.OPT_OUT]: z.boolean().default(true),
}),
[PermissionTypes.AGENTS]: agentPermissionsSchema.extend({
[Permissions.SHARED_GLOBAL]: z.boolean().default(true),
[Permissions.USE]: z.boolean().default(true),
@ -86,6 +94,13 @@ export const roleDefaults = defaultRolesSchema.parse({
[PermissionTypes.BOOKMARKS]: {
[Permissions.USE]: true,
},
[PermissionTypes.MEMORIES]: {
[Permissions.USE]: true,
[Permissions.CREATE]: true,
[Permissions.UPDATE]: true,
[Permissions.READ]: true,
[Permissions.OPT_OUT]: true,
},
[PermissionTypes.AGENTS]: {
[Permissions.SHARED_GLOBAL]: true,
[Permissions.USE]: true,
@ -110,6 +125,7 @@ export const roleDefaults = defaultRolesSchema.parse({
permissions: {
[PermissionTypes.PROMPTS]: {},
[PermissionTypes.BOOKMARKS]: {},
[PermissionTypes.MEMORIES]: {},
[PermissionTypes.AGENTS]: {},
[PermissionTypes.MULTI_CONVO]: {},
[PermissionTypes.TEMPORARY_CHAT]: {},

View file

@ -522,11 +522,19 @@ export const tMessageSchema = z.object({
feedback: feedbackSchema.optional(),
});
export type MemoryArtifact = {
key: string;
value?: string;
tokenCount?: number;
type: 'update' | 'delete';
};
export type TAttachmentMetadata = {
type?: Tools;
messageId: string;
toolCallId: string;
[Tools.web_search]?: SearchResultData;
[Tools.memory]?: MemoryArtifact;
};
export type TAttachment =

View file

@ -1,17 +1,18 @@
import type OpenAI from 'openai';
import type { InfiniteData } from '@tanstack/react-query';
import type {
TBanner,
TMessage,
TResPlugin,
ImageDetail,
TSharedLink,
TConversation,
EModelEndpoint,
TConversationTag,
TBanner,
TAttachment,
} from './schemas';
import { TMinimalFeedback } from './feedback';
import { SettingDefinition } from './generate';
import type { SettingDefinition } from './generate';
import type { TMinimalFeedback } from './feedback';
import type { Agent } from './types/assistants';
export type TOpenAIMessage = OpenAI.Chat.ChatCompletionMessageParam;
@ -20,28 +21,78 @@ export * from './schemas';
export type TMessages = TMessage[];
/* TODO: Cleanup EndpointOption types */
export type TEndpointOption = {
spec?: string | null;
iconURL?: string | null;
endpoint: EModelEndpoint;
endpointType?: EModelEndpoint;
export type TEndpointOption = Pick<
TConversation,
// Core conversation fields
| 'endpoint'
| 'endpointType'
| 'model'
| 'modelLabel'
| 'chatGptLabel'
| 'promptPrefix'
| 'temperature'
| 'topP'
| 'topK'
| 'top_p'
| 'frequency_penalty'
| 'presence_penalty'
| 'maxOutputTokens'
| 'maxContextTokens'
| 'max_tokens'
| 'maxTokens'
| 'resendFiles'
| 'imageDetail'
| 'reasoning_effort'
| 'instructions'
| 'additional_instructions'
| 'append_current_datetime'
| 'tools'
| 'stop'
| 'region'
| 'additionalModelRequestFields'
// Anthropic-specific
| 'promptCache'
| 'thinking'
| 'thinkingBudget'
// Assistant/Agent fields
| 'assistant_id'
| 'agent_id'
// UI/Display fields
| 'iconURL'
| 'greeting'
| 'spec'
// Artifacts
| 'artifacts'
// Files
| 'file_ids'
// System field
| 'system'
// Google examples
| 'examples'
// Context
| 'context'
> & {
// Fields specific to endpoint options that don't exist on TConversation
modelDisplayLabel?: string;
resendFiles?: boolean;
promptCache?: boolean;
maxContextTokens?: number;
imageDetail?: ImageDetail;
model?: string | null;
promptPrefix?: string;
temperature?: number;
chatGptLabel?: string | null;
modelLabel?: string | null;
jailbreak?: boolean;
key?: string | null;
/* assistant */
/** @deprecated Assistants API */
thread_id?: string;
/* multi-response stream */
// Conversation identifiers for multi-response streams
overrideConvoId?: string;
overrideUserMessageId?: string;
// Model parameters (used by different endpoints)
modelOptions?: Record<string, unknown>;
model_parameters?: Record<string, unknown>;
// Configuration data (added by middleware)
modelsConfig?: TModelsConfig;
// File attachments (processed by middleware)
attachments?: TAttachment[];
// Generated prompts
artifactsPrompt?: string;
// Agent-specific fields
agent?: Promise<Agent>;
// Client-specific options
clientOptions?: Record<string, unknown>;
};
export type TEphemeralAgent = {
@ -130,6 +181,9 @@ export type TUser = {
plugins?: string[];
twoFactorEnabled?: boolean;
backupCodes?: TBackupCode[];
personalization?: {
memories?: boolean;
};
createdAt: string;
updatedAt: string;
};

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-namespace */
import { StepTypes, ContentTypes, ToolCallTypes } from './runs';
import type { TAttachment, TPlugin } from 'src/schemas';
import type { FunctionToolCall } from './assistants';
import type { TAttachment } from 'src/schemas';
export namespace Agents {
export type MessageType = 'human' | 'ai' | 'generic' | 'system' | 'function' | 'tool' | 'remove';
@ -279,3 +279,79 @@ export type ToolCallResult = {
conversationId: string;
attachments?: TAttachment[];
};
export enum AuthTypeEnum {
ServiceHttp = 'service_http',
OAuth = 'oauth',
None = 'none',
}
export enum AuthorizationTypeEnum {
Bearer = 'bearer',
Basic = 'basic',
Custom = 'custom',
}
export enum TokenExchangeMethodEnum {
DefaultPost = 'default_post',
BasicAuthHeader = 'basic_auth_header',
}
export type Action = {
action_id: string;
type?: string;
settings?: Record<string, unknown>;
metadata: ActionMetadata;
version: number | string;
} & ({ assistant_id: string; agent_id?: never } | { assistant_id?: never; agent_id: string });
export type ActionMetadata = {
api_key?: string;
auth?: ActionAuth;
domain?: string;
privacy_policy_url?: string;
raw_spec?: string;
oauth_client_id?: string;
oauth_client_secret?: string;
};
export type ActionAuth = {
authorization_type?: AuthorizationTypeEnum;
custom_auth_header?: string;
type?: AuthTypeEnum;
authorization_content_type?: string;
authorization_url?: string;
client_url?: string;
scope?: string;
token_exchange_method?: TokenExchangeMethodEnum;
};
export type ActionMetadataRuntime = ActionMetadata & {
oauth_access_token?: string;
oauth_refresh_token?: string;
oauth_token_expires_at?: Date;
};
export type MCP = {
mcp_id: string;
metadata: MCPMetadata;
} & ({ assistant_id: string; agent_id?: never } | { assistant_id?: never; agent_id: string });
export type MCPMetadata = Omit<ActionMetadata, 'auth'> & {
name?: string;
description?: string;
url?: string;
tools?: string[];
auth?: MCPAuth;
icon?: string;
trust?: boolean;
};
export type MCPAuth = ActionAuth;
export type AgentToolType = {
tool_id: string;
metadata: ToolMetadata;
} & ({ assistant_id: string; agent_id?: never } | { assistant_id?: never; agent_id: string });
export type ToolMetadata = TPlugin;

View file

@ -22,6 +22,7 @@ export enum Tools {
web_search = 'web_search',
retrieval = 'retrieval',
function = 'function',
memory = 'memory',
}
export enum EToolResources {
@ -486,60 +487,6 @@ export const actionDomainSeparator = '---';
export const hostImageIdSuffix = '_host_copy';
export const hostImageNamePrefix = 'host_copy_';
export enum AuthTypeEnum {
ServiceHttp = 'service_http',
OAuth = 'oauth',
None = 'none',
}
export enum AuthorizationTypeEnum {
Bearer = 'bearer',
Basic = 'basic',
Custom = 'custom',
}
export enum TokenExchangeMethodEnum {
DefaultPost = 'default_post',
BasicAuthHeader = 'basic_auth_header',
}
export type ActionAuth = {
authorization_type?: AuthorizationTypeEnum;
custom_auth_header?: string;
type?: AuthTypeEnum;
authorization_content_type?: string;
authorization_url?: string;
client_url?: string;
scope?: string;
token_exchange_method?: TokenExchangeMethodEnum;
};
export type ActionMetadata = {
api_key?: string;
auth?: ActionAuth;
domain?: string;
privacy_policy_url?: string;
raw_spec?: string;
oauth_client_id?: string;
oauth_client_secret?: string;
};
export type ActionMetadataRuntime = ActionMetadata & {
oauth_access_token?: string;
oauth_refresh_token?: string;
oauth_token_expires_at?: Date;
};
/* Assistant types */
export type Action = {
action_id: string;
type?: string;
settings?: Record<string, unknown>;
metadata: ActionMetadata;
version: number | string;
} & ({ assistant_id: string; agent_id?: never } | { assistant_id?: never; agent_id: string });
export type AssistantAvatar = {
filepath: string;
source: string;

View file

@ -10,6 +10,7 @@ export enum FileSources {
vectordb = 'vectordb',
execute_code = 'execute_code',
mistral_ocr = 'mistral_ocr',
azure_mistral_ocr = 'azure_mistral_ocr',
text = 'text',
}

View file

@ -6,14 +6,13 @@ import {
Assistant,
AssistantCreateParams,
AssistantUpdateParams,
ActionMetadata,
FunctionTool,
AssistantDocument,
Action,
Agent,
AgentCreateParams,
AgentUpdateParams,
} from './assistants';
import { Action, ActionMetadata } from './agents';
export type MutationOptions<
Response,
@ -278,7 +277,7 @@ export type UpdatePermVars<T> = {
};
export type UpdatePromptPermVars = UpdatePermVars<p.TPromptPermissions>;
export type UpdateMemoryPermVars = UpdatePermVars<p.TMemoryPermissions>;
export type UpdateAgentPermVars = UpdatePermVars<p.TAgentPermissions>;
export type UpdatePermResponse = r.TRole;
@ -290,6 +289,13 @@ export type UpdatePromptPermOptions = MutationOptions<
types.TError | null | undefined
>;
export type UpdateMemoryPermOptions = MutationOptions<
UpdatePermResponse,
UpdateMemoryPermVars,
unknown,
types.TError | null | undefined
>;
export type UpdateAgentPermOptions = MutationOptions<
UpdatePermResponse,
UpdateAgentPermVars,

View file

@ -109,3 +109,18 @@ export type VerifyToolAuthResponse = {
export type GetToolCallParams = { conversationId: string };
export type ToolCallResults = a.ToolCallResult[];
/* Memories */
export type TUserMemory = {
key: string;
value: string;
updated_at: string;
tokenCount?: number;
};
export type MemoriesResponse = {
memories: TUserMemory[];
totalTokens: number;
tokenLimit: number | null;
usagePercentage: number | null;
};

View file

@ -5,8 +5,8 @@ import type {
SearchProviders,
TWebSearchConfig,
} from './config';
import { extractVariableName } from './utils';
import { SearchCategories, SafeSearchTypes } from './config';
import { extractVariableName } from './utils';
import { AuthType } from './schemas';
export function loadWebSearchConfig(
@ -64,23 +64,29 @@ export const webSearchAuth = {
/**
* Extracts all API keys from the webSearchAuth configuration object
*/
export const webSearchKeys: TWebSearchKeys[] = [];
export function getWebSearchKeys(): TWebSearchKeys[] {
const keys: TWebSearchKeys[] = [];
// Iterate through each category (providers, scrapers, rerankers)
for (const category of Object.keys(webSearchAuth)) {
const categoryObj = webSearchAuth[category as TWebSearchCategories];
// Iterate through each category (providers, scrapers, rerankers)
for (const category of Object.keys(webSearchAuth)) {
const categoryObj = webSearchAuth[category as TWebSearchCategories];
// Iterate through each service within the category
for (const service of Object.keys(categoryObj)) {
const serviceObj = categoryObj[service as keyof typeof categoryObj];
// Iterate through each service within the category
for (const service of Object.keys(categoryObj)) {
const serviceObj = categoryObj[service as keyof typeof categoryObj];
// Extract the API keys from the service
for (const key of Object.keys(serviceObj)) {
webSearchKeys.push(key as TWebSearchKeys);
// Extract the API keys from the service
for (const key of Object.keys(serviceObj)) {
keys.push(key as TWebSearchKeys);
}
}
}
return keys;
}
export const webSearchKeys: TWebSearchKeys[] = getWebSearchKeys();
export function extractWebSearchEnvVars({
keys,
config,