Merge branch 'dev' into feat/multi-lang-Terms-of-service

This commit is contained in:
Ruben Talstra 2025-07-04 11:01:33 +02:00 committed by GitHub
commit 97a6074edc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
660 changed files with 35171 additions and 17122 deletions

View file

@ -1,6 +1,6 @@
{
"name": "librechat-data-provider",
"version": "0.7.86",
"version": "0.7.899",
"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>;
@ -275,8 +275,7 @@ describe('ActionRequest', () => {
expect(config?.headers).toEqual({
'some-header': 'header-var',
});
expect(config?.params).toEqual({
});
expect(config?.params).toEqual({});
expect(response.data.success).toBe(true);
});
@ -285,13 +284,13 @@ describe('ActionRequest', () => {
const data: Record<string, unknown> = {
'api-version': '2025-01-01',
'message': 'a body parameter',
message: 'a body parameter',
'some-header': 'header-var',
};
const loc: Record<string, 'query' | 'path' | 'header' | 'body'> = {
'api-version': 'query',
'message': 'body',
message: 'body',
'some-header': 'header',
};
@ -326,13 +325,13 @@ describe('ActionRequest', () => {
const data: Record<string, unknown> = {
'api-version': '2025-01-01',
'message': 'a body parameter',
message: 'a body parameter',
'some-header': 'header-var',
};
const loc: Record<string, 'query' | 'path' | 'header' | 'body'> = {
'api-version': 'query',
'message': 'body',
message: 'body',
'some-header': 'header',
};
@ -367,13 +366,13 @@ describe('ActionRequest', () => {
const data: Record<string, unknown> = {
'api-version': '2025-01-01',
'message': 'a body parameter',
message: 'a body parameter',
'some-header': 'header-var',
};
const loc: Record<string, 'query' | 'path' | 'header' | 'body'> = {
'api-version': 'query',
'message': 'body',
message: 'body',
'some-header': 'header',
};
@ -443,7 +442,6 @@ describe('ActionRequest', () => {
});
expect(response.data.success).toBe(true);
});
});
it('throws an error for unsupported HTTP method', async () => {

View file

@ -1,4 +1,3 @@
/* eslint-disable jest/no-conditional-expect */
import { ZodError, z } from 'zod';
import { generateDynamicSchema, validateSettingDefinitions, OptionTypes } from '../src/generate';
import type { SettingsConfiguration } from '../src/generate';
@ -97,6 +96,37 @@ describe('generateDynamicSchema', () => {
expect(result['data']).toEqual({ testEnum: 'option2' });
});
it('should generate a schema for enum settings with empty string option', () => {
const settings: SettingsConfiguration = [
{
key: 'testEnumWithEmpty',
description: 'A test enum setting with empty string',
type: 'enum',
default: '',
options: ['', 'option1', 'option2'],
enumMappings: {
'': 'None',
option1: 'First Option',
option2: 'Second Option',
},
component: 'slider',
columnSpan: 2,
label: 'Test Enum with Empty String',
},
];
const schema = generateDynamicSchema(settings);
const result = schema.safeParse({ testEnumWithEmpty: '' });
expect(result.success).toBeTruthy();
expect(result['data']).toEqual({ testEnumWithEmpty: '' });
// Test with non-empty option
const result2 = schema.safeParse({ testEnumWithEmpty: 'option1' });
expect(result2.success).toBeTruthy();
expect(result2['data']).toEqual({ testEnumWithEmpty: 'option1' });
});
it('should fail for incorrect enum value', () => {
const settings: SettingsConfiguration = [
{
@ -481,6 +511,47 @@ describe('validateSettingDefinitions', () => {
expect(() => validateSettingDefinitions(settingsExceedingMaxTags)).toThrow(ZodError);
});
// Test for incomplete enumMappings
test('should throw error for incomplete enumMappings', () => {
const settingsWithIncompleteEnumMappings: SettingsConfiguration = [
{
key: 'displayMode',
type: 'enum',
component: 'dropdown',
options: ['light', 'dark', 'auto'],
enumMappings: {
light: 'Light Mode',
dark: 'Dark Mode',
// Missing mapping for 'auto'
},
optionType: OptionTypes.Custom,
},
];
expect(() => validateSettingDefinitions(settingsWithIncompleteEnumMappings)).toThrow(ZodError);
});
// Test for complete enumMappings including empty string
test('should not throw error for complete enumMappings including empty string', () => {
const settingsWithCompleteEnumMappings: SettingsConfiguration = [
{
key: 'selectionMode',
type: 'enum',
component: 'slider',
options: ['', 'single', 'multiple'],
enumMappings: {
'': 'None',
single: 'Single Selection',
multiple: 'Multiple Selection',
},
default: '',
optionType: OptionTypes.Custom,
},
];
expect(() => validateSettingDefinitions(settingsWithCompleteEnumMappings)).not.toThrow();
});
});
const settingsConfiguration: SettingsConfiguration = [
@ -515,7 +586,7 @@ const settingsConfiguration: SettingsConfiguration = [
{
key: 'presence_penalty',
description:
'Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model\'s likelihood to talk about new topics.',
"Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.",
type: 'number',
default: 0,
range: {
@ -529,7 +600,7 @@ const settingsConfiguration: SettingsConfiguration = [
{
key: 'frequency_penalty',
description:
'Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model\'s likelihood to repeat the same line verbatim.',
"Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.",
type: 'number',
default: 0,
range: {

View file

@ -1,277 +0,0 @@
import { StdioOptionsSchema, StreamableHTTPOptionsSchema, processMCPEnv, MCPOptions } from '../src/mcp';
describe('Environment Variable Extraction (MCP)', () => {
const originalEnv = process.env;
beforeEach(() => {
process.env = {
...originalEnv,
TEST_API_KEY: 'test-api-key-value',
ANOTHER_SECRET: 'another-secret-value',
};
});
afterEach(() => {
process.env = originalEnv;
});
describe('StdioOptionsSchema', () => {
it('should transform environment variables in the env field', () => {
const options = {
command: 'node',
args: ['server.js'],
env: {
API_KEY: '${TEST_API_KEY}',
ANOTHER_KEY: '${ANOTHER_SECRET}',
PLAIN_VALUE: 'plain-value',
NON_EXISTENT: '${NON_EXISTENT_VAR}',
},
};
const result = StdioOptionsSchema.parse(options);
expect(result.env).toEqual({
API_KEY: 'test-api-key-value',
ANOTHER_KEY: 'another-secret-value',
PLAIN_VALUE: 'plain-value',
NON_EXISTENT: '${NON_EXISTENT_VAR}',
});
});
it('should handle undefined env field', () => {
const options = {
command: 'node',
args: ['server.js'],
};
const result = StdioOptionsSchema.parse(options);
expect(result.env).toBeUndefined();
});
});
describe('StreamableHTTPOptionsSchema', () => {
it('should validate a valid streamable-http configuration', () => {
const options = {
type: 'streamable-http',
url: 'https://example.com/api',
headers: {
Authorization: 'Bearer token',
'Content-Type': 'application/json',
},
};
const result = StreamableHTTPOptionsSchema.parse(options);
expect(result).toEqual(options);
});
it('should reject websocket URLs', () => {
const options = {
type: 'streamable-http',
url: 'ws://example.com/socket',
};
expect(() => StreamableHTTPOptionsSchema.parse(options)).toThrow();
});
it('should reject secure websocket URLs', () => {
const options = {
type: 'streamable-http',
url: 'wss://example.com/socket',
};
expect(() => StreamableHTTPOptionsSchema.parse(options)).toThrow();
});
it('should require type field to be set explicitly', () => {
const options = {
url: 'https://example.com/api',
};
// 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');
});
it('should validate headers as record of strings', () => {
const options = {
type: 'streamable-http',
url: 'https://example.com/api',
headers: {
'X-API-Key': '123456',
'User-Agent': 'MCP Client',
},
};
const result = StreamableHTTPOptionsSchema.parse(options);
expect(result.headers).toEqual(options.headers);
});
});
describe('processMCPEnv', () => {
it('should create a deep clone of the input object', () => {
const originalObj: MCPOptions = {
command: 'node',
args: ['server.js'],
env: {
API_KEY: '${TEST_API_KEY}',
PLAIN_VALUE: 'plain-value',
},
};
const result = processMCPEnv(originalObj);
// Verify it's not the same object reference
expect(result).not.toBe(originalObj);
// Modify the result and ensure original is unchanged
if ('env' in result && result.env) {
result.env.API_KEY = 'modified-value';
}
expect(originalObj.env?.API_KEY).toBe('${TEST_API_KEY}');
});
it('should process environment variables in env field', () => {
const obj: MCPOptions = {
command: 'node',
args: ['server.js'],
env: {
API_KEY: '${TEST_API_KEY}',
ANOTHER_KEY: '${ANOTHER_SECRET}',
PLAIN_VALUE: 'plain-value',
NON_EXISTENT: '${NON_EXISTENT_VAR}',
},
};
const result = processMCPEnv(obj);
expect('env' in result && result.env).toEqual({
API_KEY: 'test-api-key-value',
ANOTHER_KEY: 'another-secret-value',
PLAIN_VALUE: 'plain-value',
NON_EXISTENT: '${NON_EXISTENT_VAR}',
});
});
it('should process user ID in headers field', () => {
const userId = 'test-user-123';
const obj: MCPOptions = {
type: 'sse',
url: 'https://example.com',
headers: {
Authorization: '${TEST_API_KEY}',
'User-Id': '{{LIBRECHAT_USER_ID}}',
'Content-Type': 'application/json',
},
};
const result = processMCPEnv(obj, userId);
expect('headers' in result && result.headers).toEqual({
Authorization: 'test-api-key-value',
'User-Id': 'test-user-123',
'Content-Type': 'application/json',
});
});
it('should handle null or undefined input', () => {
// @ts-ignore - Testing null/undefined handling
expect(processMCPEnv(null)).toBeNull();
// @ts-ignore - Testing null/undefined handling
expect(processMCPEnv(undefined)).toBeUndefined();
});
it('should not modify objects without env or headers', () => {
const obj: MCPOptions = {
command: 'node',
args: ['server.js'],
timeout: 5000,
};
const result = processMCPEnv(obj);
expect(result).toEqual(obj);
expect(result).not.toBe(obj); // Still a different object (deep clone)
});
it('should ensure different users with same starting config get separate values', () => {
// Create a single base configuration
const baseConfig: MCPOptions = {
type: 'sse',
url: 'https://example.com',
headers: {
'User-Id': '{{LIBRECHAT_USER_ID}}',
'API-Key': '${TEST_API_KEY}',
},
};
// Process for two different users
const user1Id = 'user-123';
const user2Id = 'user-456';
const resultUser1 = processMCPEnv(baseConfig, user1Id);
const resultUser2 = processMCPEnv(baseConfig, user2Id);
// 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);
// Verify they're different objects
expect(resultUser1).not.toBe(resultUser2);
// Modify one result and ensure it doesn't affect the other
if ('headers' in resultUser1 && resultUser1.headers) {
resultUser1.headers['User-Id'] = 'modified-user';
}
// Original config should be unchanged
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);
});
it('should process headers in streamable-http options', () => {
const userId = 'test-user-123';
const obj: MCPOptions = {
type: 'streamable-http',
url: 'https://example.com',
headers: {
Authorization: '${TEST_API_KEY}',
'User-Id': '{{LIBRECHAT_USER_ID}}',
'Content-Type': 'application/json',
},
};
const result = processMCPEnv(obj, userId);
expect('headers' in result && result.headers).toEqual({
Authorization: 'test-api-key-value',
'User-Id': 'test-user-123',
'Content-Type': 'application/json',
});
});
it('should maintain streamable-http type in processed options', () => {
const obj: MCPOptions = {
type: 'streamable-http',
url: 'https://example.com/api',
};
const result = processMCPEnv(obj);
expect(result.type).toBe('streamable-http');
});
});
});

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;
@ -303,7 +299,8 @@ class RequestExecutor {
if (this.config.parameterLocations && this.params) {
for (const key of Object.keys(this.params)) {
// Determine parameter placement; default to "query" for GET and "body" for others.
const loc: 'query' | 'path' | 'header' | 'body' = this.config.parameterLocations[key] || (method === 'get' ? 'query' : 'body');
const loc: 'query' | 'path' | 'header' | 'body' =
this.config.parameterLocations[key] || (method === 'get' ? 'query' : 'body');
const val = this.params[key];
if (loc === 'query') {
@ -351,7 +348,15 @@ export class ActionRequest {
contentType: string,
parameterLocations?: Record<string, 'query' | 'path' | 'header' | 'body'>,
) {
this.config = new RequestConfig(domain, path, method, operation, isConsequential, contentType, parameterLocations);
this.config = new RequestConfig(
domain,
path,
method,
operation,
isConsequential,
contentType,
parameterLocations,
);
}
// Add getters to maintain backward compatibility
@ -486,12 +491,12 @@ export function openapiToFunction(
}
// Record the parameter location from the OpenAPI "in" field.
paramLocations[paramName] =
(resolvedParam.in === 'query' ||
resolvedParam.in === 'path' ||
resolvedParam.in === 'header' ||
resolvedParam.in === 'body')
? resolvedParam.in
: 'query';
resolvedParam.in === 'query' ||
resolvedParam.in === 'path' ||
resolvedParam.in === 'header' ||
resolvedParam.in === 'body'
? resolvedParam.in
: 'query';
}
}

View file

@ -70,8 +70,6 @@ export const revokeUserKey = (name: string) => `${keysEndpoint}/${name}`;
export const revokeAllUserKeys = () => `${keysEndpoint}?all=true`;
export const abortRequest = (endpoint: string) => `/api/ask/${endpoint}/abort`;
export const conversationsRoot = '/api/convos';
export const conversations = (params: q.ConversationListParams) => {
@ -254,6 +252,7 @@ export const getAllPromptGroups = () => `${prompts()}/all`;
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 */
@ -272,6 +271,10 @@ export const userTerms = () => '/api/user/terms';
export const acceptUserTerms = () => '/api/user/terms/accept';
export const banner = () => '/api/banner';
// Message Feedback
export const feedback = (conversationId: string, messageId: string) =>
`/api/messages/${conversationId}/${messageId}/feedback`;
// Two-Factor Endpoints
export const enableTwoFactor = () => '/api/auth/2fa/enable';
export const verifyTwoFactor = () => '/api/auth/2fa/verify';
@ -279,3 +282,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({
@ -476,6 +481,12 @@ const termsOfServiceSchema = z.object({
export type TTermsOfService = z.infer<typeof termsOfServiceSchema>;
const mcpServersSchema = z.object({
placeholder: z.string().optional(),
});
export type TMcpServersConfig = z.infer<typeof mcpServersSchema>;
export const intefaceSchema = z
.object({
privacyPolicy: z
@ -486,16 +497,19 @@ export const intefaceSchema = z
.optional(),
termsOfService: termsOfServiceSchema.optional(),
customWelcome: z.string().optional(),
mcpServers: mcpServersSchema.optional(),
endpointsMenu: z.boolean().optional(),
modelSelect: z.boolean().optional(),
parameters: z.boolean().optional(),
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(),
temporaryChat: z.boolean().optional(),
temporaryChatRetention: z.number().min(1).max(8760).optional(),
runCode: z.boolean().optional(),
webSearch: z.boolean().optional(),
})
@ -507,6 +521,7 @@ export const intefaceSchema = z
presets: true,
multiConvo: true,
bookmarks: true,
memories: true,
prompts: true,
agents: true,
temporaryChat: true,
@ -580,11 +595,26 @@ export type TStartupConfig = {
scraperType?: ScraperTypes;
rerankerType?: RerankerTypes;
};
mcpServers?: Record<
string,
{
customUserVars: Record<
string,
{
title: string;
description: string;
}
>;
}
>;
mcpPlaceholder?: string;
};
export enum OCRStrategy {
MISTRAL_OCR = 'mistral_ocr',
CUSTOM_OCR = 'custom_ocr',
AZURE_MISTRAL_OCR = 'azure_mistral_ocr',
VERTEXAI_MISTRAL_OCR = 'vertexai_mistral_ocr',
}
export enum SearchCategories {
@ -648,11 +678,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(),
@ -693,7 +747,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(),
})
@ -852,7 +906,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',
@ -896,19 +949,11 @@ export const initialModelsConfig: TModelsConfig = {
[EModelEndpoint.bedrock]: defaultModels[EModelEndpoint.bedrock],
};
export const EndpointURLs: { [key in EModelEndpoint]: string } = {
[EModelEndpoint.openAI]: `/api/ask/${EModelEndpoint.openAI}`,
[EModelEndpoint.google]: `/api/ask/${EModelEndpoint.google}`,
[EModelEndpoint.custom]: `/api/ask/${EModelEndpoint.custom}`,
[EModelEndpoint.anthropic]: `/api/ask/${EModelEndpoint.anthropic}`,
[EModelEndpoint.gptPlugins]: `/api/ask/${EModelEndpoint.gptPlugins}`,
[EModelEndpoint.azureOpenAI]: `/api/ask/${EModelEndpoint.azureOpenAI}`,
[EModelEndpoint.chatGPTBrowser]: `/api/ask/${EModelEndpoint.chatGPTBrowser}`,
[EModelEndpoint.azureAssistants]: '/api/assistants/v1/chat',
export const EndpointURLs = {
[EModelEndpoint.assistants]: '/api/assistants/v2/chat',
[EModelEndpoint.azureAssistants]: '/api/assistants/v1/chat',
[EModelEndpoint.agents]: `/api/${EModelEndpoint.agents}/chat`,
[EModelEndpoint.bedrock]: `/api/${EModelEndpoint.bedrock}/chat`,
};
} as const;
export const modularEndpoints = new Set<EModelEndpoint | string>([
EModelEndpoint.gptPlugins,
@ -1103,6 +1148,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)
*/
@ -1207,6 +1256,10 @@ export enum ErrorTypes {
* Google provider returned an error
*/
GOOGLE_ERROR = 'google_error',
/**
* Google provider does not allow custom tools with built-in tools
*/
GOOGLE_TOOL_CONFLICT = 'google_tool_conflict',
/**
* Invalid Agent Provider (excluded by Admin)
*/
@ -1290,6 +1343,10 @@ export enum SettingsTabValues {
* Chat input commands
*/
COMMANDS = 'commands',
/**
* Tab for Personalization Settings
*/
PERSONALIZATION = 'personalization',
}
export enum STTProviders {
@ -1325,9 +1382,9 @@ export enum TTSProviders {
/** Enum for app-wide constants */
export enum Constants {
/** Key for the app's version. */
VERSION = 'v0.7.8',
VERSION = 'v0.7.9-rc1',
/** 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 */
@ -1354,6 +1411,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',
}
@ -1397,6 +1456,18 @@ 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_',
/** Last checked toggle for File Search per conversation ID */
LAST_FILE_SEARCH_TOGGLE_ = 'LAST_FILE_SEARCH_TOGGLE_',
/** Key for the last selected agent provider */
LAST_AGENT_PROVIDER = 'lastAgentProvider',
/** Key for the last selected agent model */
LAST_AGENT_MODEL = 'lastAgentModel',
/** Pin state for MCP tools per conversation ID */
PIN_MCP_ = 'PIN_MCP_',
/** Pin state for Web Search per conversation ID */
PIN_WEB_SEARCH_ = 'PIN_WEB_SEARCH_',
/** Pin state for Code Interpreter per conversation ID */
PIN_CODE_INTERPRETER_ = 'PIN_CODE_INTERPRETER_',
}
export enum ForkOptions {

View file

@ -11,31 +11,31 @@ export default function createPayload(submission: t.TSubmission) {
isContinued,
isTemporary,
ephemeralAgent,
editedContent,
} = submission;
const { conversationId } = s.tConvoUpdateSchema.parse(conversation);
const { endpoint, endpointType } = endpointOption as {
const { endpoint: _e, endpointType } = endpointOption as {
endpoint: s.EModelEndpoint;
endpointType?: s.EModelEndpoint;
};
let server = EndpointURLs[endpointType ?? endpoint];
const isEphemeral = s.isEphemeralAgent(endpoint, ephemeralAgent);
if (isEdited && s.isAssistantsEndpoint(endpoint)) {
server += '/modify';
} else if (isEdited) {
server = server.replace('/ask/', '/edit/');
} else if (isEphemeral) {
server = `${EndpointURLs[s.EModelEndpoint.agents]}/${endpoint}`;
const endpoint = _e as s.EModelEndpoint;
let server = `${EndpointURLs[s.EModelEndpoint.agents]}/${endpoint}`;
if (s.isAssistantsEndpoint(endpoint)) {
server =
EndpointURLs[(endpointType ?? endpoint) as 'assistants' | 'azureAssistants'] +
(isEdited ? '/modify' : '');
}
const payload: t.TPayload = {
...userMessage,
...endpointOption,
ephemeralAgent: isEphemeral ? ephemeralAgent : undefined,
endpoint,
ephemeralAgent: s.isAssistantsEndpoint(endpoint) ? undefined : ephemeralAgent,
isContinued: !!(isEdited && isContinued),
conversationId,
isTemporary,
editedContent,
};
return { server, payload };

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';
@ -10,14 +11,6 @@ import request from './request';
import * as s from './schemas';
import * as r from './roles';
export function abortRequestWithMessage(
endpoint: string,
abortKey: string,
message: string,
): Promise<void> {
return request.post(endpoints.abortRequest(endpoint), { arg: { abortKey, message } });
}
export function revokeUserKey(name: string): Promise<unknown> {
return request.delete(endpoints.revokeUserKey(name));
}
@ -150,7 +143,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 +348,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 +404,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`,
@ -718,6 +715,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());
@ -765,6 +768,15 @@ export function getBanner(): Promise<t.TBannerResponse> {
return request.get(endpoints.banner());
}
export function updateFeedback(
conversationId: string,
messageId: string,
payload: t.TUpdateFeedbackRequest,
): Promise<t.TUpdateFeedbackResponse> {
return request.put(endpoints.feedback(conversationId, messageId), payload);
}
// 2FA
export function enableTwoFactor(): Promise<t.TEnable2FAResponse> {
return request.get(endpoints.enableTwoFactor());
}
@ -790,3 +802,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

@ -0,0 +1,141 @@
import { z } from 'zod';
export type TFeedbackRating = 'thumbsUp' | 'thumbsDown';
export const FEEDBACK_RATINGS = ['thumbsUp', 'thumbsDown'] as const;
export const FEEDBACK_REASON_KEYS = [
// Down
'not_matched',
'inaccurate',
'bad_style',
'missing_image',
'unjustified_refusal',
'not_helpful',
'other',
// Up
'accurate_reliable',
'creative_solution',
'clear_well_written',
'attention_to_detail',
] as const;
export type TFeedbackTagKey = (typeof FEEDBACK_REASON_KEYS)[number];
export interface TFeedbackTag {
key: TFeedbackTagKey;
label: string;
direction: TFeedbackRating;
icon: string;
}
// --- Tag Registry ---
export const FEEDBACK_TAGS: TFeedbackTag[] = [
// Thumbs Down
{
key: 'not_matched',
label: 'com_ui_feedback_tag_not_matched',
direction: 'thumbsDown',
icon: 'AlertCircle',
},
{
key: 'inaccurate',
label: 'com_ui_feedback_tag_inaccurate',
direction: 'thumbsDown',
icon: 'AlertCircle',
},
{
key: 'bad_style',
label: 'com_ui_feedback_tag_bad_style',
direction: 'thumbsDown',
icon: 'PenTool',
},
{
key: 'missing_image',
label: 'com_ui_feedback_tag_missing_image',
direction: 'thumbsDown',
icon: 'ImageOff',
},
{
key: 'unjustified_refusal',
label: 'com_ui_feedback_tag_unjustified_refusal',
direction: 'thumbsDown',
icon: 'Ban',
},
{
key: 'not_helpful',
label: 'com_ui_feedback_tag_not_helpful',
direction: 'thumbsDown',
icon: 'ThumbsDown',
},
{
key: 'other',
label: 'com_ui_feedback_tag_other',
direction: 'thumbsDown',
icon: 'HelpCircle',
},
// Thumbs Up
{
key: 'accurate_reliable',
label: 'com_ui_feedback_tag_accurate_reliable',
direction: 'thumbsUp',
icon: 'CheckCircle',
},
{
key: 'creative_solution',
label: 'com_ui_feedback_tag_creative_solution',
direction: 'thumbsUp',
icon: 'Lightbulb',
},
{
key: 'clear_well_written',
label: 'com_ui_feedback_tag_clear_well_written',
direction: 'thumbsUp',
icon: 'PenTool',
},
{
key: 'attention_to_detail',
label: 'com_ui_feedback_tag_attention_to_detail',
direction: 'thumbsUp',
icon: 'Search',
},
];
export function getTagsForRating(rating: TFeedbackRating): TFeedbackTag[] {
return FEEDBACK_TAGS.filter((tag) => tag.direction === rating);
}
export const feedbackTagKeySchema = z.enum(FEEDBACK_REASON_KEYS);
export const feedbackRatingSchema = z.enum(FEEDBACK_RATINGS);
export const feedbackSchema = z.object({
rating: feedbackRatingSchema,
tag: feedbackTagKeySchema,
text: z.string().max(1024).optional(),
});
export type TMinimalFeedback = z.infer<typeof feedbackSchema>;
export type TFeedback = {
rating: TFeedbackRating;
tag: TFeedbackTag | undefined;
text?: string;
};
export function toMinimalFeedback(feedback: TFeedback | undefined): TMinimalFeedback | undefined {
if (!feedback?.rating || !feedback?.tag || !feedback.tag.key) {
return undefined;
}
return {
rating: feedback.rating,
tag: feedback.tag.key,
text: feedback.text,
};
}
export function getTagByKey(key: TFeedbackTagKey | undefined): TFeedbackTag | undefined {
if (!key) {
return undefined;
}
return FEEDBACK_TAGS.find((tag) => tag.key === key);
}

View file

@ -1,7 +1,6 @@
/* eslint-disable max-len */
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,
@ -50,6 +49,8 @@ export const fullMimeTypesList = [
'text/javascript',
'image/gif',
'image/png',
'image/heic',
'image/heif',
'application/x-tar',
'application/typescript',
'application/xml',
@ -81,6 +82,8 @@ export const codeInterpreterMimeTypesList = [
'text/javascript',
'image/gif',
'image/png',
'image/heic',
'image/heif',
'application/x-tar',
'application/typescript',
'application/xml',
@ -106,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,
@ -139,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',
@ -156,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)))$/,
];
@ -188,6 +192,12 @@ export const fileConfig = {
},
serverFileSizeLimit: defaultSizeLimit,
avatarSizeLimit: mbToBytes(2),
clientImageResize: {
enabled: false,
maxWidth: 1900,
maxHeight: 1900,
quality: 0.92,
},
checkType: function (fileType: string, supportedTypes: RegExp[] = supportedMimeTypes) {
return supportedTypes.some((regex) => regex.test(fileType));
},
@ -228,6 +238,14 @@ export const fileConfigSchema = z.object({
px: z.number().min(0).optional(),
})
.optional(),
clientImageResize: z
.object({
enabled: z.boolean().optional(),
maxWidth: z.number().min(0).optional(),
maxHeight: z.number().min(0).optional(),
quality: z.number().min(0).max(1).optional(),
})
.optional(),
});
/** Helper function to safely convert string patterns to RegExp objects */
@ -256,6 +274,14 @@ export function mergeFileConfig(dynamic: z.infer<typeof fileConfigSchema> | unde
mergedConfig.avatarSizeLimit = mbToBytes(dynamic.avatarSizeLimit);
}
// Merge clientImageResize configuration
if (dynamic.clientImageResize !== undefined) {
mergedConfig.clientImageResize = {
...mergedConfig.clientImageResize,
...dynamic.clientImageResize,
};
}
if (!dynamic.endpoints) {
return mergedConfig;
}

View file

@ -467,7 +467,11 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
}
/* Default value checks */
if (setting.type === SettingTypes.Number && isNaN(setting.default as number) && setting.default != null) {
if (
setting.type === SettingTypes.Number &&
isNaN(setting.default as number) &&
setting.default != null
) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must be a number.`,
@ -475,7 +479,11 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
});
}
if (setting.type === SettingTypes.Boolean && typeof setting.default !== 'boolean' && setting.default != null) {
if (
setting.type === SettingTypes.Boolean &&
typeof setting.default !== 'boolean' &&
setting.default != null
) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must be a boolean.`,
@ -485,7 +493,8 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
if (
(setting.type === SettingTypes.String || setting.type === SettingTypes.Enum) &&
typeof setting.default !== 'string' && setting.default != null
typeof setting.default !== 'string' &&
setting.default != null
) {
errors.push({
code: ZodIssueCode.custom,
@ -520,6 +529,19 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
path: ['default'],
});
}
// Validate enumMappings
if (setting.enumMappings && setting.type === SettingTypes.Enum && setting.options) {
for (const option of setting.options) {
if (!(option in setting.enumMappings)) {
errors.push({
code: ZodIssueCode.custom,
message: `Missing enumMapping for option "${option}" in setting ${setting.key}.`,
path: ['enumMappings'],
});
}
}
}
}
if (errors.length > 0) {

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';
@ -39,4 +41,6 @@ import * as dataService from './data-service';
export * from './utils';
export * from './actions';
export { default as createPayload } from './createPayload';
/* feedback */
export * from './feedback';
export * from './parameterSettings';

View file

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

View file

@ -1,4 +1,5 @@
import { z } from 'zod';
import { TokenExchangeMethodEnum } from './types/agents';
import { extractEnvVariable } from './utils';
const BaseOptionsSchema = z.object({
@ -7,6 +8,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({
@ -112,41 +152,3 @@ export const MCPOptionsSchema = z.union([
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
*/
export function processMCPEnv(obj: Readonly<MCPOptions>, userId?: string): MCPOptions {
if (obj === null || obj === undefined) {
return obj;
}
const newObj: MCPOptions = structuredClone(obj);
if ('env' in newObj && newObj.env) {
const processedEnv: Record<string, string> = {};
for (const [key, value] of Object.entries(newObj.env)) {
processedEnv[key] = extractEnvVariable(value);
}
newObj.env = processedEnv;
} else 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);
}
newObj.headers = processedHeaders;
}
if ('url' in newObj && newObj.url) {
newObj.url = extractEnvVariable(newObj.url);
}
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

@ -4,6 +4,7 @@ import {
openAISettings,
googleSettings,
ReasoningEffort,
ReasoningSummary,
BedrockProviders,
anthropicSettings,
} from './types';
@ -71,6 +72,11 @@ const baseDefinitions: Record<string, SettingDefinition> = {
default: ImageDetail.auto,
component: 'slider',
options: [ImageDetail.low, ImageDetail.auto, ImageDetail.high],
enumMappings: {
[ImageDetail.low]: 'com_ui_low',
[ImageDetail.auto]: 'com_ui_auto',
[ImageDetail.high]: 'com_ui_high',
},
optionType: 'conversation',
columnSpan: 2,
},
@ -83,7 +89,7 @@ const createDefinition = (
return { ...base, ...overrides } as SettingDefinition;
};
const librechat: Record<string, SettingDefinition> = {
export const librechat = {
modelLabel: {
key: 'modelLabel',
label: 'com_endpoint_custom_name',
@ -94,7 +100,7 @@ const librechat: Record<string, SettingDefinition> = {
placeholder: 'com_endpoint_openai_custom_name_placeholder',
placeholderCode: true,
optionType: 'conversation',
},
} as const,
maxContextTokens: {
key: 'maxContextTokens',
label: 'com_endpoint_context_tokens',
@ -107,7 +113,7 @@ const librechat: Record<string, SettingDefinition> = {
descriptionCode: true,
optionType: 'model',
columnSpan: 2,
},
} as const,
resendFiles: {
key: 'resendFiles',
label: 'com_endpoint_plug_resend_files',
@ -120,7 +126,7 @@ const librechat: Record<string, SettingDefinition> = {
optionType: 'conversation',
showDefault: false,
columnSpan: 2,
},
} as const,
promptPrefix: {
key: 'promptPrefix',
label: 'com_endpoint_prompt_prefix',
@ -131,7 +137,7 @@ const librechat: Record<string, SettingDefinition> = {
placeholder: 'com_endpoint_openai_prompt_prefix_placeholder',
placeholderCode: true,
optionType: 'model',
},
} as const,
};
const openAIParams: Record<string, SettingDefinition> = {
@ -211,9 +217,70 @@ const openAIParams: Record<string, SettingDefinition> = {
description: 'com_endpoint_openai_reasoning_effort',
descriptionCode: true,
type: 'enum',
default: ReasoningEffort.medium,
default: ReasoningEffort.none,
component: 'slider',
options: [ReasoningEffort.low, ReasoningEffort.medium, ReasoningEffort.high],
options: [
ReasoningEffort.none,
ReasoningEffort.low,
ReasoningEffort.medium,
ReasoningEffort.high,
],
enumMappings: {
[ReasoningEffort.none]: 'com_ui_none',
[ReasoningEffort.low]: 'com_ui_low',
[ReasoningEffort.medium]: 'com_ui_medium',
[ReasoningEffort.high]: 'com_ui_high',
},
optionType: 'model',
columnSpan: 4,
},
useResponsesApi: {
key: 'useResponsesApi',
label: 'com_endpoint_use_responses_api',
labelCode: true,
description: 'com_endpoint_openai_use_responses_api',
descriptionCode: true,
type: 'boolean',
default: false,
component: 'switch',
optionType: 'model',
showDefault: false,
columnSpan: 2,
},
web_search: {
key: 'web_search',
label: 'com_ui_web_search',
labelCode: true,
description: 'com_endpoint_openai_use_web_search',
descriptionCode: true,
type: 'boolean',
default: false,
component: 'switch',
optionType: 'model',
showDefault: false,
columnSpan: 2,
},
reasoning_summary: {
key: 'reasoning_summary',
label: 'com_endpoint_reasoning_summary',
labelCode: true,
description: 'com_endpoint_openai_reasoning_summary',
descriptionCode: true,
type: 'enum',
default: ReasoningSummary.none,
component: 'slider',
options: [
ReasoningSummary.none,
ReasoningSummary.auto,
ReasoningSummary.concise,
ReasoningSummary.detailed,
],
enumMappings: {
[ReasoningSummary.none]: 'com_ui_none',
[ReasoningSummary.auto]: 'com_ui_auto',
[ReasoningSummary.concise]: 'com_ui_concise',
[ReasoningSummary.detailed]: 'com_ui_detailed',
},
optionType: 'model',
columnSpan: 4,
},
@ -347,7 +414,9 @@ const bedrock: Record<string, SettingDefinition> = {
labelCode: true,
type: 'number',
component: 'input',
placeholder: 'com_endpoint_anthropic_maxoutputtokens',
description: 'com_endpoint_anthropic_maxoutputtokens',
descriptionCode: true,
placeholder: 'com_nav_theme_system',
placeholderCode: true,
optionType: 'model',
columnSpan: 2,
@ -450,6 +519,50 @@ const google: Record<string, SettingDefinition> = {
optionType: 'model',
columnSpan: 2,
},
thinking: {
key: 'thinking',
label: 'com_endpoint_thinking',
labelCode: true,
description: 'com_endpoint_google_thinking',
descriptionCode: true,
type: 'boolean',
default: googleSettings.thinking.default,
component: 'switch',
optionType: 'conversation',
showDefault: false,
columnSpan: 2,
},
thinkingBudget: {
key: 'thinkingBudget',
label: 'com_endpoint_thinking_budget',
labelCode: true,
description: 'com_endpoint_google_thinking_budget',
descriptionCode: true,
placeholder: 'com_ui_auto',
placeholderCode: true,
type: 'number',
component: 'input',
range: {
min: googleSettings.thinkingBudget.min,
max: googleSettings.thinkingBudget.max,
step: googleSettings.thinkingBudget.step,
},
optionType: 'conversation',
columnSpan: 2,
},
grounding: {
key: 'grounding',
label: 'com_endpoint_use_search_grounding',
labelCode: true,
description: 'com_endpoint_google_use_search_grounding',
descriptionCode: true,
type: 'boolean',
default: false,
component: 'switch',
optionType: 'model',
showDefault: false,
columnSpan: 2,
},
};
const googleConfig: SettingsConfiguration = [
@ -461,6 +574,9 @@ const googleConfig: SettingsConfiguration = [
google.topP,
google.topK,
librechat.resendFiles,
google.thinking,
google.thinkingBudget,
google.grounding,
];
const googleCol1: SettingsConfiguration = [
@ -476,6 +592,9 @@ const googleCol2: SettingsConfiguration = [
google.topP,
google.topK,
librechat.resendFiles,
google.thinking,
google.thinkingBudget,
google.grounding,
];
const openAI: SettingsConfiguration = [
@ -490,7 +609,10 @@ const openAI: SettingsConfiguration = [
baseDefinitions.stop,
librechat.resendFiles,
baseDefinitions.imageDetail,
openAIParams.web_search,
openAIParams.reasoning_effort,
openAIParams.useResponsesApi,
openAIParams.reasoning_summary,
];
const openAICol1: SettingsConfiguration = [
@ -507,9 +629,12 @@ const openAICol2: SettingsConfiguration = [
openAIParams.frequency_penalty,
openAIParams.presence_penalty,
baseDefinitions.stop,
openAIParams.reasoning_effort,
librechat.resendFiles,
baseDefinitions.imageDetail,
openAIParams.reasoning_effort,
openAIParams.reasoning_summary,
openAIParams.useResponsesApi,
openAIParams.web_search,
];
const anthropicConfig: SettingsConfiguration = [

View file

@ -122,19 +122,6 @@ export function errorsToString(errors: ZodIssue[]) {
.join(' ');
}
/** Resolves header values to env variables if detected */
export function resolveHeaders(headers: Record<string, string> | undefined) {
const resolvedHeaders = { ...(headers ?? {}) };
if (headers && typeof headers === 'object' && !Array.isArray(headers)) {
Object.keys(headers).forEach((key) => {
resolvedHeaders[key] = extractEnvVariable(headers[key]);
});
}
return resolvedHeaders;
}
export function getFirstDefinedValue(possibleValues: string[]) {
let returnValue;
for (const value of possibleValues) {
@ -225,13 +212,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 ?? '';
@ -273,15 +262,11 @@ export const getResponseSender = (endpointOption: t.TEndpointOption): string =>
if (endpoint === EModelEndpoint.google) {
if (modelLabel) {
return modelLabel;
} else if (model && (model.includes('gemini') || model.includes('learnlm'))) {
return 'Gemini';
} else if (model?.toLowerCase().includes('gemma') === true) {
return 'Gemma';
} else if (model && model.includes('code')) {
return 'Codey';
}
return 'PaLM2';
return 'Gemini';
}
if (endpoint === EModelEndpoint.custom || endpointType === EModelEndpoint.custom) {

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

@ -12,23 +12,6 @@ import { QueryKeys } from '../keys';
import * as s from '../schemas';
import * as t from '../types';
export const useAbortRequestWithMessage = (): UseMutationResult<
void,
Error,
{ endpoint: string; abortKey: string; message: string }
> => {
const queryClient = useQueryClient();
return useMutation(
({ endpoint, abortKey, message }) =>
dataService.abortRequestWithMessage(endpoint, abortKey, message),
{
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.balance]);
},
},
);
};
export const useGetSharedMessages = (
shareId: string,
config?: UseQueryOptions<t.TSharedMessagesResponse>,
@ -347,3 +330,19 @@ export const useGetCustomConfigSpeechQuery = (
},
);
};
export const useUpdateFeedbackMutation = (
conversationId: string,
messageId: string,
): UseMutationResult<t.TUpdateFeedbackResponse, Error, t.TUpdateFeedbackRequest> => {
const queryClient = useQueryClient();
return useMutation(
(payload: t.TUpdateFeedbackRequest) =>
dataService.updateFeedback(conversationId, messageId, payload),
{
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.messages, messageId]);
},
},
);
};

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

@ -1,8 +1,8 @@
import { z } from 'zod';
import { Tools } from './types/assistants';
import type { TMessageContentParts, FunctionTool, FunctionToolCall } from './types/assistants';
import { TFeedback, feedbackSchema } from './feedback';
import type { SearchResultData } from './types/web';
import type { TEphemeralAgent } from './types';
import type { TFile } from './types/files';
export const isUUID = z.string().uuid();
@ -90,22 +90,6 @@ export const isAgentsEndpoint = (_endpoint?: EModelEndpoint.agents | null | stri
return endpoint === EModelEndpoint.agents;
};
export const isEphemeralAgent = (
endpoint?: EModelEndpoint.agents | null | string,
ephemeralAgent?: TEphemeralAgent | null,
) => {
if (!ephemeralAgent) {
return false;
}
if (isAgentsEndpoint(endpoint)) {
return false;
}
const hasMCPSelected = (ephemeralAgent?.mcp?.length ?? 0) > 0;
const hasCodeSelected = (ephemeralAgent?.execute_code ?? false) === true;
const hasSearchSelected = (ephemeralAgent?.web_search ?? false) === true;
return hasMCPSelected || hasCodeSelected || hasSearchSelected;
};
export const isParamEndpoint = (
endpoint: EModelEndpoint | string,
endpointType?: EModelEndpoint | string,
@ -128,11 +112,19 @@ export enum ImageDetail {
}
export enum ReasoningEffort {
none = '',
low = 'low',
medium = 'medium',
high = 'high',
}
export enum ReasoningSummary {
none = '',
auto = 'auto',
concise = 'concise',
detailed = 'detailed',
}
export const imageDetailNumeric = {
[ImageDetail.low]: 0,
[ImageDetail.auto]: 1,
@ -147,6 +139,7 @@ export const imageDetailValue = {
export const eImageDetailSchema = z.nativeEnum(ImageDetail);
export const eReasoningEffortSchema = z.nativeEnum(ReasoningEffort);
export const eReasoningSummarySchema = z.nativeEnum(ReasoningSummary);
export const defaultAssistantFormValues = {
assistant: '',
@ -271,6 +264,18 @@ export const googleSettings = {
step: 1 as const,
default: 40 as const,
},
thinking: {
default: true as const,
},
thinkingBudget: {
min: -1 as const,
max: 32768 as const,
step: 1 as const,
/** `-1` = Dynamic Thinking, meaning the model will adjust
* the budget based on the complexity of the request.
*/
default: -1 as const,
},
};
const ANTHROPIC_MAX_OUTPUT = 128000 as const;
@ -416,7 +421,7 @@ export type TPluginAuthConfig = z.infer<typeof tPluginAuthConfigSchema>;
export const tPluginSchema = z.object({
name: z.string(),
pluginKey: z.string(),
description: z.string(),
description: z.string().optional(),
icon: z.string().optional(),
authConfig: z.array(tPluginAuthConfigSchema).optional(),
authenticated: z.boolean().optional(),
@ -498,6 +503,7 @@ export const tMessageSchema = z.object({
title: z.string().nullable().or(z.literal('New Chat')).default('New Chat'),
sender: z.string().optional(),
text: z.string(),
/** @deprecated */
generation: z.string().nullable().optional(),
isCreatedByUser: z.boolean(),
error: z.boolean().optional(),
@ -518,13 +524,22 @@ export const tMessageSchema = z.object({
thread_id: z.string().optional(),
/* frontend components */
iconURL: z.string().nullable().optional(),
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 =
@ -543,6 +558,7 @@ export type TMessage = z.input<typeof tMessageSchema> & {
siblingIndex?: number;
attachments?: TAttachment[];
clientTimestamp?: string;
feedback?: TFeedback;
};
export const coerceNumber = z.union([z.number(), z.string()]).transform((val) => {
@ -613,8 +629,15 @@ export const tConversationSchema = z.object({
file_ids: z.array(z.string()).optional(),
/* vision */
imageDetail: eImageDetailSchema.optional(),
/* OpenAI: o1 only */
reasoning_effort: eReasoningEffortSchema.optional(),
/* OpenAI: Reasoning models only */
reasoning_effort: eReasoningEffortSchema.optional().nullable(),
reasoning_summary: eReasoningSummarySchema.optional().nullable(),
/* OpenAI: use Responses API */
useResponsesApi: z.boolean().optional(),
/* OpenAI: use Responses API with Web Search */
web_search: z.boolean().optional(),
/* Google: use Search Grounding */
grounding: z.boolean().optional(),
/* assistant */
assistant_id: z.string().optional(),
/* agents */
@ -711,6 +734,14 @@ export const tQueryParamsSchema = tConversationSchema
top_p: true,
/** @endpoints openAI, custom, azureOpenAI */
max_tokens: true,
/** @endpoints openAI, custom, azureOpenAI */
reasoning_effort: true,
/** @endpoints openAI, custom, azureOpenAI */
reasoning_summary: true,
/** @endpoints openAI, custom, azureOpenAI */
useResponsesApi: true,
/** @endpoints google */
grounding: true,
/** @endpoints google, anthropic, bedrock */
topP: true,
/** @endpoints google, anthropic */
@ -791,6 +822,9 @@ export const googleBaseSchema = tConversationSchema.pick({
artifacts: true,
topP: true,
topK: true,
thinking: true,
thinkingBudget: true,
grounding: true,
iconURL: true,
greeting: true,
spec: true,
@ -816,6 +850,13 @@ export const googleGenConfigSchema = z
presencePenalty: coerceNumber.optional(),
frequencyPenalty: coerceNumber.optional(),
stopSequences: z.array(z.string()).optional(),
thinkingConfig: z
.object({
includeThoughts: z.boolean().optional(),
thinkingBudget: coerceNumber.optional(),
})
.optional(),
grounding: z.boolean().optional(),
})
.strip()
.optional();
@ -1030,10 +1071,13 @@ export const openAIBaseSchema = tConversationSchema.pick({
maxContextTokens: true,
max_tokens: true,
reasoning_effort: true,
reasoning_summary: true,
useResponsesApi: true,
web_search: true,
});
export const openAISchema = openAIBaseSchema
.transform((obj: Partial<TConversation>) => removeNullishValues(obj))
.transform((obj: Partial<TConversation>) => removeNullishValues(obj, true))
.catch(() => ({}));
export const compactGoogleSchema = googleBaseSchema

View file

@ -1,16 +1,19 @@
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 { 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;
export * from './schemas';
@ -18,33 +21,84 @@ 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 = {
mcp?: string[];
web_search?: boolean;
file_search?: boolean;
execute_code?: boolean;
};
@ -55,6 +109,11 @@ export type TPayload = Partial<TMessage> &
messages?: TMessages;
isTemporary: boolean;
ephemeralAgent?: TEphemeralAgent | null;
editedContent?: {
index: number;
text: string;
type: 'text' | 'think';
} | null;
};
export type TSubmission = {
@ -73,6 +132,11 @@ export type TSubmission = {
endpointOption: TEndpointOption;
clientTimestamp?: string;
ephemeralAgent?: TEphemeralAgent | null;
editedContent?: {
index: number;
text: string;
type: 'text' | 'think';
} | null;
};
export type EventSubmission = Omit<TSubmission, 'initialResponse'> & { initialResponse: TMessage };
@ -80,7 +144,7 @@ export type EventSubmission = Omit<TSubmission, 'initialResponse'> & { initialRe
export type TPluginAction = {
pluginKey: string;
action: 'install' | 'uninstall';
auth?: Partial<Record<string, string>>;
auth?: Partial<Record<string, string>> | null;
isEntityTool?: boolean;
};
@ -90,7 +154,7 @@ export type TUpdateUserPlugins = {
isEntityTool?: boolean;
pluginKey: string;
action: string;
auth?: Partial<Record<string, string | null>>;
auth?: Partial<Record<string, string | null>> | null;
};
// TODO `label` needs to be changed to the proper `TranslationKeys`
@ -128,6 +192,9 @@ export type TUser = {
plugins?: string[];
twoFactorEnabled?: boolean;
backupCodes?: TBackupCode[];
personalization?: {
memories?: boolean;
};
createdAt: string;
updatedAt: string;
};
@ -547,6 +614,16 @@ export type TAcceptTermsResponse = {
export type TBannerResponse = TBanner | null;
export type TUpdateFeedbackRequest = {
feedback?: TMinimalFeedback;
};
export type TUpdateFeedbackResponse = {
messageId: string;
conversationId: string;
feedback?: TMinimalFeedback;
};
export type TBalanceResponse = {
tokenCredits: number;
// Automatic refill settings

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,8 @@ export enum FileSources {
vectordb = 'vectordb',
execute_code = 'execute_code',
mistral_ocr = 'mistral_ocr',
azure_mistral_ocr = 'azure_mistral_ocr',
vertexai_mistral_ocr = 'vertexai_mistral_ocr',
text = 'text',
}
@ -47,6 +49,12 @@ export type FileConfig = {
};
serverFileSizeLimit?: number;
avatarSizeLimit?: number;
clientImageResize?: {
enabled?: boolean;
maxWidth?: number;
maxHeight?: number;
quality?: number;
};
checkType?: (fileType: string, supportedTypes: RegExp[]) => boolean;
};

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,

View file

@ -264,19 +264,19 @@ describe('convertJsonSchemaToZod', () => {
properties: {
name: {
type: 'string',
description: 'The user\'s name',
description: "The user's name",
},
age: {
type: 'number',
description: 'The user\'s age',
description: "The user's age",
},
},
};
const zodSchema = convertJsonSchemaToZod(schema);
const shape = (zodSchema as z.ZodObject<any>).shape;
expect(shape.name.description).toBe('The user\'s name');
expect(shape.age.description).toBe('The user\'s age');
expect(shape.name.description).toBe("The user's name");
expect(shape.age.description).toBe("The user's age");
});
it('should preserve descriptions in nested objects', () => {
@ -290,7 +290,7 @@ describe('convertJsonSchemaToZod', () => {
properties: {
name: {
type: 'string',
description: 'The user\'s name',
description: "The user's name",
},
settings: {
type: 'object',
@ -318,7 +318,7 @@ describe('convertJsonSchemaToZod', () => {
const userShape = shape.user instanceof z.ZodObject ? shape.user.shape : {};
if ('name' in userShape && 'settings' in userShape) {
expect(userShape.name.description).toBe('The user\'s name');
expect(userShape.name.description).toBe("The user's name");
expect(userShape.settings.description).toBe('User preferences');
const settingsShape =
@ -682,10 +682,7 @@ describe('convertJsonSchemaToZod', () => {
name: { type: 'string' },
age: { type: 'number' },
},
anyOf: [
{ required: ['name'] },
{ required: ['age'] },
],
anyOf: [{ required: ['name'] }, { required: ['age'] }],
oneOf: [
{ properties: { role: { type: 'string', enum: ['admin'] } } },
{ properties: { role: { type: 'string', enum: ['user'] } } },
@ -708,7 +705,7 @@ describe('convertJsonSchemaToZod', () => {
it('should drop fields from nested schemas', () => {
// Create a schema with nested fields that should be dropped
const schema: JsonSchemaType & {
properties?: Record<string, JsonSchemaType & { anyOf?: any; oneOf?: any }>
properties?: Record<string, JsonSchemaType & { anyOf?: any; oneOf?: any }>;
} = {
type: 'object',
properties: {
@ -718,10 +715,7 @@ describe('convertJsonSchemaToZod', () => {
name: { type: 'string' },
role: { type: 'string' },
},
anyOf: [
{ required: ['name'] },
{ required: ['role'] },
],
anyOf: [{ required: ['name'] }, { required: ['role'] }],
},
settings: {
type: 'object',
@ -742,20 +736,24 @@ describe('convertJsonSchemaToZod', () => {
});
// The schema should still validate normal properties
expect(zodSchema?.parse({
user: { name: 'John', role: 'admin' },
settings: { theme: 'custom' }, // This would fail if oneOf was still present
})).toEqual({
expect(
zodSchema?.parse({
user: { name: 'John', role: 'admin' },
settings: { theme: 'custom' }, // This would fail if oneOf was still present
}),
).toEqual({
user: { name: 'John', role: 'admin' },
settings: { theme: 'custom' },
});
// But the anyOf constraint should be gone from user
// (If it was present, this would fail because neither name nor role is required)
expect(zodSchema?.parse({
user: {},
settings: { theme: 'light' },
})).toEqual({
expect(
zodSchema?.parse({
user: {},
settings: { theme: 'light' },
}),
).toEqual({
user: {},
settings: { theme: 'light' },
});
@ -803,10 +801,7 @@ describe('convertJsonSchemaToZod', () => {
anyOf: [{ minItems: 1 }],
},
},
oneOf: [
{ required: ['name', 'permissions'] },
{ required: ['name'] },
],
oneOf: [{ required: ['name', 'permissions'] }, { required: ['name'] }],
},
},
},
@ -871,10 +866,7 @@ describe('convertJsonSchemaToZod', () => {
const schema = {
type: 'object', // Add a type to satisfy JsonSchemaType
properties: {}, // Empty properties
oneOf: [
{ type: 'string' },
{ type: 'number' },
],
oneOf: [{ type: 'string' }, { type: 'number' }],
} as JsonSchemaType & { oneOf?: any };
// Convert with transformOneOfAnyOf option
@ -893,10 +885,7 @@ describe('convertJsonSchemaToZod', () => {
const schema = {
type: 'object', // Add a type to satisfy JsonSchemaType
properties: {}, // Empty properties
anyOf: [
{ type: 'string' },
{ type: 'number' },
],
anyOf: [{ type: 'string' }, { type: 'number' }],
} as JsonSchemaType & { anyOf?: any };
// Convert with transformOneOfAnyOf option
@ -956,10 +945,7 @@ describe('convertJsonSchemaToZod', () => {
properties: {
value: { type: 'string' },
},
oneOf: [
{ required: ['value'] },
{ properties: { optional: { type: 'boolean' } } },
],
oneOf: [{ required: ['value'] }, { properties: { optional: { type: 'boolean' } } }],
} as JsonSchemaType & { oneOf?: any };
// Convert with transformOneOfAnyOf option
@ -1013,9 +999,12 @@ describe('convertJsonSchemaToZod', () => {
},
},
} as JsonSchemaType & {
properties?: Record<string, JsonSchemaType & {
properties?: Record<string, JsonSchemaType & { oneOf?: any }>
}>
properties?: Record<
string,
JsonSchemaType & {
properties?: Record<string, JsonSchemaType & { oneOf?: any }>;
}
>;
};
// Convert with transformOneOfAnyOf option
@ -1024,14 +1013,16 @@ describe('convertJsonSchemaToZod', () => {
});
// The schema should validate nested unions
expect(zodSchema?.parse({
user: {
contact: {
type: 'email',
email: 'test@example.com',
expect(
zodSchema?.parse({
user: {
contact: {
type: 'email',
email: 'test@example.com',
},
},
},
})).toEqual({
}),
).toEqual({
user: {
contact: {
type: 'email',
@ -1040,14 +1031,16 @@ describe('convertJsonSchemaToZod', () => {
},
});
expect(zodSchema?.parse({
user: {
contact: {
type: 'phone',
phone: '123-456-7890',
expect(
zodSchema?.parse({
user: {
contact: {
type: 'phone',
phone: '123-456-7890',
},
},
},
})).toEqual({
}),
).toEqual({
user: {
contact: {
type: 'phone',
@ -1057,14 +1050,16 @@ describe('convertJsonSchemaToZod', () => {
});
// Should reject invalid contact types
expect(() => zodSchema?.parse({
user: {
contact: {
type: 'email',
phone: '123-456-7890', // Missing email, has phone instead
expect(() =>
zodSchema?.parse({
user: {
contact: {
type: 'email',
phone: '123-456-7890', // Missing email, has phone instead
},
},
},
})).toThrow();
}),
).toThrow();
});
it('should work with dropFields option', () => {
@ -1072,10 +1067,7 @@ describe('convertJsonSchemaToZod', () => {
const schema = {
type: 'object', // Add a type to satisfy JsonSchemaType
properties: {}, // Empty properties
oneOf: [
{ type: 'string' },
{ type: 'number' },
],
oneOf: [{ type: 'string' }, { type: 'number' }],
deprecated: true, // Field to drop
} as JsonSchemaType & { oneOf?: any; deprecated?: boolean };