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

This commit is contained in:
Ruben Talstra 2025-05-29 17:06:50 +02:00 committed by GitHub
commit 126b1fe412
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
323 changed files with 20207 additions and 4039 deletions

View file

@ -187,6 +187,8 @@ export const agents = ({ path = '', options }: { path?: string; options?: object
return url;
};
export const revertAgentVersion = (agent_id: string) => `${agents({ path: `${agent_id}/revert` })}`;
export const files = () => '/api/files';
export const images = () => `${files()}/images`;

View file

@ -119,7 +119,10 @@ export const bedrockInputParser = s.tConversationSchema
/** Default thinking and thinkingBudget for 'anthropic.claude-3-7-sonnet' models, if not defined */
if (
typeof typedData.model === 'string' &&
typedData.model.includes('anthropic.claude-3-7-sonnet')
(typedData.model.includes('anthropic.claude-3-7-sonnet') ||
/anthropic\.claude-(?:[4-9](?:\.\d+)?(?:-\d+)?-(?:sonnet|opus|haiku)|(?:sonnet|opus|haiku)-[4-9])/.test(
typedData.model,
))
) {
if (additionalFields.thinking === undefined) {
additionalFields.thinking = true;

View file

@ -7,7 +7,7 @@ import { fileConfigSchema } from './file-config';
import { FileSources } from './types/files';
import { MCPServersSchema } from './mcp';
export const defaultSocialLogins = ['google', 'facebook', 'openid', 'github', 'discord'];
export const defaultSocialLogins = ['google', 'facebook', 'openid', 'github', 'discord', 'saml'];
export const defaultRetrievalModels = [
'gpt-4o',
@ -52,6 +52,7 @@ export const excludedKeys = new Set([
'model',
'files',
'spec',
'disableParams',
]);
export enum SettingsViews {
@ -166,6 +167,7 @@ export enum AgentCapabilities {
end_after_tools = 'end_after_tools',
execute_code = 'execute_code',
file_search = 'file_search',
web_search = 'web_search',
artifacts = 'artifacts',
actions = 'actions',
tools = 'tools',
@ -231,6 +233,17 @@ export const assistantEndpointSchema = baseEndpointSchema.merge(
export type TAssistantEndpoint = z.infer<typeof assistantEndpointSchema>;
export const defaultAgentCapabilities = [
AgentCapabilities.execute_code,
AgentCapabilities.file_search,
AgentCapabilities.web_search,
AgentCapabilities.artifacts,
AgentCapabilities.actions,
AgentCapabilities.tools,
AgentCapabilities.chain,
AgentCapabilities.ocr,
];
export const agentsEndpointSChema = baseEndpointSchema.merge(
z.object({
/* agents specific */
@ -241,15 +254,7 @@ export const agentsEndpointSChema = baseEndpointSchema.merge(
capabilities: z
.array(z.nativeEnum(AgentCapabilities))
.optional()
.default([
AgentCapabilities.execute_code,
AgentCapabilities.file_search,
AgentCapabilities.artifacts,
AgentCapabilities.actions,
AgentCapabilities.tools,
AgentCapabilities.ocr,
AgentCapabilities.chain,
]),
.default(defaultAgentCapabilities),
}),
);
@ -278,6 +283,12 @@ export const endpointSchema = baseEndpointSchema.merge(
headers: z.record(z.any()).optional(),
addParams: z.record(z.any()).optional(),
dropParams: z.array(z.string()).optional(),
customParams: z
.object({
defaultParamsEndpoint: z.string().default('custom'),
paramDefinitions: z.array(z.record(z.any())).optional(),
})
.strict(),
customOrder: z.number().optional(),
directEndpoint: z.boolean().optional(),
titleMessageRole: z.string().optional(),
@ -486,6 +497,7 @@ export const intefaceSchema = z
agents: z.boolean().optional(),
temporaryChat: z.boolean().optional(),
runCode: z.boolean().optional(),
webSearch: z.boolean().optional(),
})
.default({
endpointsMenu: true,
@ -499,6 +511,7 @@ export const intefaceSchema = z
agents: true,
temporaryChat: true,
runCode: true,
webSearch: true,
});
export type TInterfaceConfig = z.infer<typeof intefaceSchema>;
@ -533,9 +546,12 @@ export type TStartupConfig = {
googleLoginEnabled: boolean;
openidLoginEnabled: boolean;
appleLoginEnabled: boolean;
samlLoginEnabled: boolean;
openidLabel: string;
openidImageUrl: string;
openidAutoRedirect: boolean;
samlLabel: string;
samlImageUrl: string;
/** LDAP Auth Configuration */
ldap?: {
/** LDAP enabled */
@ -559,6 +575,11 @@ export type TStartupConfig = {
instanceProjectId: string;
bundlerURL?: string;
staticBundlerURL?: string;
webSearch?: {
searchProvider?: SearchProviders;
scraperType?: ScraperTypes;
rerankerType?: RerankerTypes;
};
};
export enum OCRStrategy {
@ -566,10 +587,52 @@ export enum OCRStrategy {
CUSTOM_OCR = 'custom_ocr',
}
export enum SearchCategories {
PROVIDERS = 'providers',
SCRAPERS = 'scrapers',
RERANKERS = 'rerankers',
}
export enum SearchProviders {
SERPER = 'serper',
SEARXNG = 'searxng',
}
export enum ScraperTypes {
FIRECRAWL = 'firecrawl',
SERPER = 'serper',
}
export enum RerankerTypes {
JINA = 'jina',
COHERE = 'cohere',
}
export enum SafeSearchTypes {
OFF = 0,
MODERATE = 1,
STRICT = 2,
}
export const webSearchSchema = z.object({
serperApiKey: z.string().optional().default('${SERPER_API_KEY}'),
firecrawlApiKey: z.string().optional().default('${FIRECRAWL_API_KEY}'),
firecrawlApiUrl: z.string().optional().default('${FIRECRAWL_API_URL}'),
jinaApiKey: z.string().optional().default('${JINA_API_KEY}'),
cohereApiKey: z.string().optional().default('${COHERE_API_KEY}'),
searchProvider: z.nativeEnum(SearchProviders).optional(),
scraperType: z.nativeEnum(ScraperTypes).optional(),
rerankerType: z.nativeEnum(RerankerTypes).optional(),
scraperTimeout: z.number().optional(),
safeSearch: z.nativeEnum(SafeSearchTypes).default(SafeSearchTypes.MODERATE),
});
export type TWebSearchConfig = z.infer<typeof webSearchSchema>;
export const ocrSchema = z.object({
mistralModel: z.string().optional(),
apiKey: z.string().optional().default('OCR_API_KEY'),
baseURL: z.string().optional().default('OCR_BASEURL'),
apiKey: z.string().optional().default('${OCR_API_KEY}'),
baseURL: z.string().optional().default('${OCR_BASEURL}'),
strategy: z.nativeEnum(OCRStrategy).default(OCRStrategy.MISTRAL_OCR),
});
@ -589,6 +652,7 @@ export const configSchema = z.object({
version: z.string(),
cache: z.boolean().default(true),
ocr: ocrSchema.optional(),
webSearch: webSearchSchema.optional(),
secureImageLinks: z.boolean().optional(),
imageOutputType: z.nativeEnum(EImageOutputType).default(EImageOutputType.PNG),
includedTools: z.array(z.string()).optional(),
@ -727,6 +791,10 @@ const sharedOpenAIModels = [
];
const sharedAnthropicModels = [
'claude-sonnet-4-20250514',
'claude-sonnet-4-latest',
'claude-opus-4-20250514',
'claude-opus-4-latest',
'claude-3-7-sonnet-latest',
'claude-3-7-sonnet-20250219',
'claude-3-5-haiku-20241022',
@ -886,8 +954,7 @@ export const visionModels = [
'gemma',
'gemini-exp',
'gemini-1.5',
'gemini-2.0',
'gemini-2.5',
'gemini-2',
'gemini-3',
'moondream',
'llama3.2-vision',
@ -895,6 +962,10 @@ export const visionModels = [
'llama-3-2-11b-vision',
'llama-3.2-90b-vision',
'llama-3-2-90b-vision',
'llama-4',
'claude-opus-4',
'claude-sonnet-4',
'claude-haiku-4',
];
export enum VisionModes {
generative = 'generative',
@ -1040,6 +1111,10 @@ export enum CacheKeys {
* Key for s3 check intervals per user
*/
S3_EXPIRY_INTERVAL = 'S3_EXPIRY_INTERVAL',
/**
* key for open id exchanged tokens
*/
OPENID_EXCHANGED_TOKENS = 'OPENID_EXCHANGED_TOKENS',
}
/**
@ -1203,6 +1278,10 @@ export enum SettingsTabValues {
* Tab for Data Controls
*/
DATA = 'data',
/**
* Tab for Balance Settings
*/
BALANCE = 'balance',
/**
* Tab for Account Settings
*/
@ -1248,7 +1327,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.5',
CONFIG_VERSION = '1.2.6',
/** 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 */
@ -1316,6 +1395,8 @@ export enum LocalStorageKeys {
LAST_MCP_ = 'LAST_MCP_',
/** Last checked toggle for Code Interpreter API per conversation ID */
LAST_CODE_TOGGLE_ = 'LAST_CODE_TOGGLE_',
/** Last checked toggle for Web Search per conversation ID */
LAST_WEB_SEARCH_TOGGLE_ = 'LAST_WEB_SEARCH_TOGGLE_',
}
export enum ForkOptions {

View file

@ -93,7 +93,7 @@ export function getUser(): Promise<t.TUser> {
return request.get(endpoints.user());
}
export function getUserBalance(): Promise<string> {
export function getUserBalance(): Promise<t.TBalanceResponse> {
return request.get(endpoints.balance());
}
@ -431,6 +431,14 @@ export const listAgents = (params: a.AgentListParams): Promise<a.AgentListRespon
);
};
export const revertAgentVersion = ({
agent_id,
version_index,
}: {
agent_id: string;
version_index: number;
}): Promise<a.Agent> => request.post(endpoints.revertAgentVersion(agent_id), { version_index });
/* Tools */
export const getAvailableAgentTools = (): Promise<s.TPlugin[]> => {

View file

@ -222,6 +222,12 @@ export const fileConfigSchema = z.object({
endpoints: z.record(endpointFileConfigSchema).optional(),
serverFileSizeLimit: z.number().min(0).optional(),
avatarSizeLimit: z.number().min(0).optional(),
imageGeneration: z
.object({
percentage: z.number().min(0).max(100).optional(),
px: z.number().min(0).optional(),
})
.optional(),
});
/** Helper function to safely convert string patterns to RegExp objects */

View file

@ -358,7 +358,7 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
// continue;
}
setting.includeInput =
setting.type === SettingTypes.Number ? setting.includeInput ?? true : false; // Default to true if type is number
setting.type === SettingTypes.Number ? (setting.includeInput ?? true) : false; // Default to true if type is number
}
if (setting.component === ComponentTypes.Slider && setting.type === SettingTypes.Number) {
@ -445,7 +445,8 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
// Validate optionType and conversation schema
if (setting.optionType !== OptionTypes.Custom) {
const conversationSchema = tConversationSchema.shape[setting.key as keyof TConversation];
const conversationSchema =
tConversationSchema.shape[setting.key as keyof Omit<TConversation, 'disableParams'>];
if (!conversationSchema) {
errors.push({
code: ZodIssueCode.custom,
@ -466,7 +467,7 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
}
/* Default value checks */
if (setting.type === SettingTypes.Number && isNaN(setting.default as number)) {
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.`,
@ -474,7 +475,7 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
});
}
if (setting.type === SettingTypes.Boolean && typeof setting.default !== 'boolean') {
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.`,
@ -484,7 +485,7 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
if (
(setting.type === SettingTypes.String || setting.type === SettingTypes.Enum) &&
typeof setting.default !== 'string'
typeof setting.default !== 'string' && setting.default != null
) {
errors.push({
code: ZodIssueCode.custom,

View file

@ -14,6 +14,8 @@ export * from './generate';
export * from './models';
/* mcp */
export * from './mcp';
/* web search */
export * from './web';
/* RBAC */
export * from './permissions';
export * from './roles';
@ -25,6 +27,7 @@ export * from './types/files';
export * from './types/mutations';
export * from './types/queries';
export * from './types/runs';
export * from './types/web';
/* query/mutation keys */
export * from './keys';
/* api call helpers */
@ -36,3 +39,4 @@ import * as dataService from './data-service';
export * from './utils';
export * from './actions';
export { default as createPayload } from './createPayload';
export * from './parameterSettings';

View file

@ -65,6 +65,7 @@ export enum MutationKeys {
updateAgentAction = 'updateAgentAction',
deleteAction = 'deleteAction',
deleteAgentAction = 'deleteAgentAction',
revertAgentVersion = 'revertAgentVersion',
deleteUser = 'deleteUser',
updateRole = 'updateRole',
enableTwoFactor = 'enableTwoFactor',

View file

@ -53,9 +53,10 @@ export const WebSocketOptionsSchema = BaseOptionsSchema.extend({
type: z.literal('websocket').optional(),
url: z
.string()
.url()
.transform((val: string) => extractEnvVariable(val))
.pipe(z.string().url())
.refine(
(val) => {
(val: string) => {
const protocol = new URL(val).protocol;
return protocol === 'ws:' || protocol === 'wss:';
},
@ -70,9 +71,10 @@ export const SSEOptionsSchema = BaseOptionsSchema.extend({
headers: z.record(z.string(), z.string()).optional(),
url: z
.string()
.url()
.transform((val: string) => extractEnvVariable(val))
.pipe(z.string().url())
.refine(
(val) => {
(val: string) => {
const protocol = new URL(val).protocol;
return protocol !== 'ws:' && protocol !== 'wss:';
},
@ -85,15 +87,19 @@ export const SSEOptionsSchema = BaseOptionsSchema.extend({
export const StreamableHTTPOptionsSchema = BaseOptionsSchema.extend({
type: z.literal('streamable-http'),
headers: z.record(z.string(), z.string()).optional(),
url: z.string().url().refine(
(val) => {
url: z
.string()
.transform((val: string) => extractEnvVariable(val))
.pipe(z.string().url())
.refine(
(val: string) => {
const protocol = new URL(val).protocol;
return protocol !== 'ws:' && protocol !== 'wss:';
},
{
message: 'Streamable HTTP URL must not start with ws:// or wss://',
},
),
),
});
export const MCPOptionsSchema = z.union([
@ -138,5 +144,9 @@ export function processMCPEnv(obj: Readonly<MCPOptions>, userId?: string): MCPOp
newObj.headers = processedHeaders;
}
if ('url' in newObj && newObj.url) {
newObj.url = extractEnvVariable(newObj.url);
}
return newObj;
}

View file

@ -0,0 +1,726 @@
import {
ImageDetail,
EModelEndpoint,
openAISettings,
googleSettings,
ReasoningEffort,
BedrockProviders,
anthropicSettings,
} from './types';
import { SettingDefinition, SettingsConfiguration } from './generate';
// Base definitions
const baseDefinitions: Record<string, SettingDefinition> = {
model: {
key: 'model',
label: 'com_ui_model',
labelCode: true,
type: 'string',
component: 'dropdown',
optionType: 'model',
selectPlaceholder: 'com_ui_select_model',
searchPlaceholder: 'com_ui_select_search_model',
searchPlaceholderCode: true,
selectPlaceholderCode: true,
columnSpan: 4,
},
temperature: {
key: 'temperature',
label: 'com_endpoint_temperature',
labelCode: true,
description: 'com_endpoint_openai_temp',
descriptionCode: true,
type: 'number',
component: 'slider',
optionType: 'model',
columnSpan: 4,
},
topP: {
key: 'topP',
label: 'com_endpoint_top_p',
labelCode: true,
description: 'com_endpoint_anthropic_topp',
descriptionCode: true,
type: 'number',
component: 'slider',
optionType: 'model',
columnSpan: 4,
},
stop: {
key: 'stop',
label: 'com_endpoint_stop',
labelCode: true,
description: 'com_endpoint_openai_stop',
descriptionCode: true,
placeholder: 'com_endpoint_stop_placeholder',
placeholderCode: true,
type: 'array',
default: [],
component: 'tags',
optionType: 'conversation',
minTags: 0,
maxTags: 4,
},
imageDetail: {
key: 'imageDetail',
label: 'com_endpoint_plug_image_detail',
labelCode: true,
description: 'com_endpoint_openai_detail',
descriptionCode: true,
type: 'enum',
default: ImageDetail.auto,
component: 'slider',
options: [ImageDetail.low, ImageDetail.auto, ImageDetail.high],
optionType: 'conversation',
columnSpan: 2,
},
};
const createDefinition = (
base: Partial<SettingDefinition>,
overrides: Partial<SettingDefinition>,
): SettingDefinition => {
return { ...base, ...overrides } as SettingDefinition;
};
const librechat: Record<string, SettingDefinition> = {
modelLabel: {
key: 'modelLabel',
label: 'com_endpoint_custom_name',
labelCode: true,
type: 'string',
default: '',
component: 'input',
placeholder: 'com_endpoint_openai_custom_name_placeholder',
placeholderCode: true,
optionType: 'conversation',
},
maxContextTokens: {
key: 'maxContextTokens',
label: 'com_endpoint_context_tokens',
labelCode: true,
type: 'number',
component: 'input',
placeholder: 'com_nav_theme_system',
placeholderCode: true,
description: 'com_endpoint_context_info',
descriptionCode: true,
optionType: 'model',
columnSpan: 2,
},
resendFiles: {
key: 'resendFiles',
label: 'com_endpoint_plug_resend_files',
labelCode: true,
description: 'com_endpoint_openai_resend_files',
descriptionCode: true,
type: 'boolean',
default: true,
component: 'switch',
optionType: 'conversation',
showDefault: false,
columnSpan: 2,
},
promptPrefix: {
key: 'promptPrefix',
label: 'com_endpoint_prompt_prefix',
labelCode: true,
type: 'string',
default: '',
component: 'textarea',
placeholder: 'com_endpoint_openai_prompt_prefix_placeholder',
placeholderCode: true,
optionType: 'model',
},
};
const openAIParams: Record<string, SettingDefinition> = {
chatGptLabel: {
...librechat.modelLabel,
key: 'chatGptLabel',
},
promptPrefix: librechat.promptPrefix,
temperature: createDefinition(baseDefinitions.temperature, {
default: openAISettings.temperature.default,
range: {
min: openAISettings.temperature.min,
max: openAISettings.temperature.max,
step: openAISettings.temperature.step,
},
}),
top_p: createDefinition(baseDefinitions.topP, {
key: 'top_p',
default: openAISettings.top_p.default,
range: {
min: openAISettings.top_p.min,
max: openAISettings.top_p.max,
step: openAISettings.top_p.step,
},
}),
frequency_penalty: {
key: 'frequency_penalty',
label: 'com_endpoint_frequency_penalty',
labelCode: true,
description: 'com_endpoint_openai_freq',
descriptionCode: true,
type: 'number',
default: openAISettings.frequency_penalty.default,
range: {
min: openAISettings.frequency_penalty.min,
max: openAISettings.frequency_penalty.max,
step: openAISettings.frequency_penalty.step,
},
component: 'slider',
optionType: 'model',
columnSpan: 4,
},
presence_penalty: {
key: 'presence_penalty',
label: 'com_endpoint_presence_penalty',
labelCode: true,
description: 'com_endpoint_openai_pres',
descriptionCode: true,
type: 'number',
default: openAISettings.presence_penalty.default,
range: {
min: openAISettings.presence_penalty.min,
max: openAISettings.presence_penalty.max,
step: openAISettings.presence_penalty.step,
},
component: 'slider',
optionType: 'model',
columnSpan: 4,
},
max_tokens: {
key: 'max_tokens',
label: 'com_endpoint_max_output_tokens',
labelCode: true,
type: 'number',
component: 'input',
description: 'com_endpoint_openai_max_tokens',
descriptionCode: true,
placeholder: 'com_nav_theme_system',
placeholderCode: true,
optionType: 'model',
columnSpan: 2,
},
reasoning_effort: {
key: 'reasoning_effort',
label: 'com_endpoint_reasoning_effort',
labelCode: true,
description: 'com_endpoint_openai_reasoning_effort',
descriptionCode: true,
type: 'enum',
default: ReasoningEffort.medium,
component: 'slider',
options: [ReasoningEffort.low, ReasoningEffort.medium, ReasoningEffort.high],
optionType: 'model',
columnSpan: 4,
},
};
const anthropic: Record<string, SettingDefinition> = {
maxOutputTokens: {
key: 'maxOutputTokens',
label: 'com_endpoint_max_output_tokens',
labelCode: true,
type: 'number',
component: 'input',
description: 'com_endpoint_anthropic_maxoutputtokens',
descriptionCode: true,
placeholder: 'com_nav_theme_system',
placeholderCode: true,
range: {
min: anthropicSettings.maxOutputTokens.min,
max: anthropicSettings.maxOutputTokens.max,
step: anthropicSettings.maxOutputTokens.step,
},
optionType: 'model',
columnSpan: 2,
},
temperature: createDefinition(baseDefinitions.temperature, {
default: anthropicSettings.temperature.default,
range: {
min: anthropicSettings.temperature.min,
max: anthropicSettings.temperature.max,
step: anthropicSettings.temperature.step,
},
}),
topP: createDefinition(baseDefinitions.topP, {
default: anthropicSettings.topP.default,
range: {
min: anthropicSettings.topP.min,
max: anthropicSettings.topP.max,
step: anthropicSettings.topP.step,
},
}),
topK: {
key: 'topK',
label: 'com_endpoint_top_k',
labelCode: true,
description: 'com_endpoint_anthropic_topk',
descriptionCode: true,
type: 'number',
default: anthropicSettings.topK.default,
range: {
min: anthropicSettings.topK.min,
max: anthropicSettings.topK.max,
step: anthropicSettings.topK.step,
},
component: 'slider',
optionType: 'model',
columnSpan: 4,
},
promptCache: {
key: 'promptCache',
label: 'com_endpoint_prompt_cache',
labelCode: true,
description: 'com_endpoint_anthropic_prompt_cache',
descriptionCode: true,
type: 'boolean',
default: anthropicSettings.promptCache.default,
component: 'switch',
optionType: 'conversation',
showDefault: false,
columnSpan: 2,
},
thinking: {
key: 'thinking',
label: 'com_endpoint_thinking',
labelCode: true,
description: 'com_endpoint_anthropic_thinking',
descriptionCode: true,
type: 'boolean',
default: anthropicSettings.thinking.default,
component: 'switch',
optionType: 'conversation',
showDefault: false,
columnSpan: 2,
},
thinkingBudget: {
key: 'thinkingBudget',
label: 'com_endpoint_thinking_budget',
labelCode: true,
description: 'com_endpoint_anthropic_thinking_budget',
descriptionCode: true,
type: 'number',
component: 'input',
default: anthropicSettings.thinkingBudget.default,
range: {
min: anthropicSettings.thinkingBudget.min,
max: anthropicSettings.thinkingBudget.max,
step: anthropicSettings.thinkingBudget.step,
},
optionType: 'conversation',
columnSpan: 2,
},
};
const bedrock: Record<string, SettingDefinition> = {
system: {
key: 'system',
label: 'com_endpoint_prompt_prefix',
labelCode: true,
type: 'string',
default: '',
component: 'textarea',
placeholder: 'com_endpoint_openai_prompt_prefix_placeholder',
placeholderCode: true,
optionType: 'model',
},
region: {
key: 'region',
type: 'string',
label: 'com_ui_region',
labelCode: true,
component: 'combobox',
optionType: 'conversation',
selectPlaceholder: 'com_ui_select_region',
searchPlaceholder: 'com_ui_select_search_region',
searchPlaceholderCode: true,
selectPlaceholderCode: true,
columnSpan: 2,
},
maxTokens: {
key: 'maxTokens',
label: 'com_endpoint_max_output_tokens',
labelCode: true,
type: 'number',
component: 'input',
placeholder: 'com_endpoint_anthropic_maxoutputtokens',
placeholderCode: true,
optionType: 'model',
columnSpan: 2,
},
temperature: createDefinition(baseDefinitions.temperature, {
default: 1,
range: { min: 0, max: 1, step: 0.01 },
}),
topK: createDefinition(anthropic.topK, {
range: { min: 0, max: 500, step: 1 },
}),
topP: createDefinition(baseDefinitions.topP, {
default: 0.999,
range: { min: 0, max: 1, step: 0.01 },
}),
};
const mistral: Record<string, SettingDefinition> = {
temperature: createDefinition(baseDefinitions.temperature, {
default: 0.7,
range: { min: 0, max: 1, step: 0.01 },
}),
topP: createDefinition(baseDefinitions.topP, {
range: { min: 0, max: 1, step: 0.01 },
}),
};
const cohere: Record<string, SettingDefinition> = {
temperature: createDefinition(baseDefinitions.temperature, {
default: 0.3,
range: { min: 0, max: 1, step: 0.01 },
}),
topP: createDefinition(baseDefinitions.topP, {
default: 0.75,
range: { min: 0.01, max: 0.99, step: 0.01 },
}),
};
const meta: Record<string, SettingDefinition> = {
temperature: createDefinition(baseDefinitions.temperature, {
default: 0.5,
range: { min: 0, max: 1, step: 0.01 },
}),
topP: createDefinition(baseDefinitions.topP, {
default: 0.9,
range: { min: 0, max: 1, step: 0.01 },
}),
};
const google: Record<string, SettingDefinition> = {
temperature: createDefinition(baseDefinitions.temperature, {
default: googleSettings.temperature.default,
range: {
min: googleSettings.temperature.min,
max: googleSettings.temperature.max,
step: googleSettings.temperature.step,
},
}),
topP: createDefinition(baseDefinitions.topP, {
default: googleSettings.topP.default,
range: {
min: googleSettings.topP.min,
max: googleSettings.topP.max,
step: googleSettings.topP.step,
},
}),
topK: {
key: 'topK',
label: 'com_endpoint_top_k',
labelCode: true,
description: 'com_endpoint_google_topk',
descriptionCode: true,
type: 'number',
default: googleSettings.topK.default,
range: {
min: googleSettings.topK.min,
max: googleSettings.topK.max,
step: googleSettings.topK.step,
},
component: 'slider',
optionType: 'model',
columnSpan: 4,
},
maxOutputTokens: {
key: 'maxOutputTokens',
label: 'com_endpoint_max_output_tokens',
labelCode: true,
type: 'number',
component: 'input',
description: 'com_endpoint_google_maxoutputtokens',
descriptionCode: true,
placeholder: 'com_nav_theme_system',
placeholderCode: true,
default: googleSettings.maxOutputTokens.default,
range: {
min: googleSettings.maxOutputTokens.min,
max: googleSettings.maxOutputTokens.max,
step: googleSettings.maxOutputTokens.step,
},
optionType: 'model',
columnSpan: 2,
},
};
const googleConfig: SettingsConfiguration = [
librechat.modelLabel,
librechat.promptPrefix,
librechat.maxContextTokens,
google.maxOutputTokens,
google.temperature,
google.topP,
google.topK,
librechat.resendFiles,
];
const googleCol1: SettingsConfiguration = [
baseDefinitions.model as SettingDefinition,
librechat.modelLabel,
librechat.promptPrefix,
];
const googleCol2: SettingsConfiguration = [
librechat.maxContextTokens,
google.maxOutputTokens,
google.temperature,
google.topP,
google.topK,
librechat.resendFiles,
];
const openAI: SettingsConfiguration = [
librechat.modelLabel,
librechat.promptPrefix,
librechat.maxContextTokens,
openAIParams.max_tokens,
openAIParams.temperature,
openAIParams.top_p,
openAIParams.frequency_penalty,
openAIParams.presence_penalty,
baseDefinitions.stop,
librechat.resendFiles,
baseDefinitions.imageDetail,
openAIParams.reasoning_effort,
];
const openAICol1: SettingsConfiguration = [
baseDefinitions.model as SettingDefinition,
librechat.modelLabel,
librechat.promptPrefix,
];
const openAICol2: SettingsConfiguration = [
librechat.maxContextTokens,
openAIParams.max_tokens,
openAIParams.temperature,
openAIParams.top_p,
openAIParams.frequency_penalty,
openAIParams.presence_penalty,
baseDefinitions.stop,
openAIParams.reasoning_effort,
librechat.resendFiles,
baseDefinitions.imageDetail,
];
const anthropicConfig: SettingsConfiguration = [
librechat.modelLabel,
librechat.promptPrefix,
librechat.maxContextTokens,
anthropic.maxOutputTokens,
anthropic.temperature,
anthropic.topP,
anthropic.topK,
librechat.resendFiles,
anthropic.promptCache,
anthropic.thinking,
anthropic.thinkingBudget,
];
const anthropicCol1: SettingsConfiguration = [
baseDefinitions.model as SettingDefinition,
librechat.modelLabel,
librechat.promptPrefix,
];
const anthropicCol2: SettingsConfiguration = [
librechat.maxContextTokens,
anthropic.maxOutputTokens,
anthropic.temperature,
anthropic.topP,
anthropic.topK,
librechat.resendFiles,
anthropic.promptCache,
anthropic.thinking,
anthropic.thinkingBudget,
];
const bedrockAnthropic: SettingsConfiguration = [
librechat.modelLabel,
bedrock.system,
librechat.maxContextTokens,
bedrock.maxTokens,
bedrock.temperature,
bedrock.topP,
bedrock.topK,
baseDefinitions.stop,
librechat.resendFiles,
bedrock.region,
anthropic.thinking,
anthropic.thinkingBudget,
];
const bedrockMistral: SettingsConfiguration = [
librechat.modelLabel,
librechat.promptPrefix,
librechat.maxContextTokens,
bedrock.maxTokens,
mistral.temperature,
mistral.topP,
librechat.resendFiles,
bedrock.region,
];
const bedrockCohere: SettingsConfiguration = [
librechat.modelLabel,
librechat.promptPrefix,
librechat.maxContextTokens,
bedrock.maxTokens,
cohere.temperature,
cohere.topP,
librechat.resendFiles,
bedrock.region,
];
const bedrockGeneral: SettingsConfiguration = [
librechat.modelLabel,
librechat.promptPrefix,
librechat.maxContextTokens,
meta.temperature,
meta.topP,
librechat.resendFiles,
bedrock.region,
];
const bedrockAnthropicCol1: SettingsConfiguration = [
baseDefinitions.model as SettingDefinition,
librechat.modelLabel,
bedrock.system,
baseDefinitions.stop,
];
const bedrockAnthropicCol2: SettingsConfiguration = [
librechat.maxContextTokens,
bedrock.maxTokens,
bedrock.temperature,
bedrock.topP,
bedrock.topK,
librechat.resendFiles,
bedrock.region,
anthropic.thinking,
anthropic.thinkingBudget,
];
const bedrockMistralCol1: SettingsConfiguration = [
baseDefinitions.model as SettingDefinition,
librechat.modelLabel,
librechat.promptPrefix,
];
const bedrockMistralCol2: SettingsConfiguration = [
librechat.maxContextTokens,
bedrock.maxTokens,
mistral.temperature,
mistral.topP,
librechat.resendFiles,
bedrock.region,
];
const bedrockCohereCol1: SettingsConfiguration = [
baseDefinitions.model as SettingDefinition,
librechat.modelLabel,
librechat.promptPrefix,
];
const bedrockCohereCol2: SettingsConfiguration = [
librechat.maxContextTokens,
bedrock.maxTokens,
cohere.temperature,
cohere.topP,
librechat.resendFiles,
bedrock.region,
];
const bedrockGeneralCol1: SettingsConfiguration = [
baseDefinitions.model as SettingDefinition,
librechat.modelLabel,
librechat.promptPrefix,
];
const bedrockGeneralCol2: SettingsConfiguration = [
librechat.maxContextTokens,
meta.temperature,
meta.topP,
librechat.resendFiles,
bedrock.region,
];
export const paramSettings: Record<string, SettingsConfiguration | undefined> = {
[EModelEndpoint.openAI]: openAI,
[EModelEndpoint.azureOpenAI]: openAI,
[EModelEndpoint.custom]: openAI,
[EModelEndpoint.anthropic]: anthropicConfig,
[`${EModelEndpoint.bedrock}-${BedrockProviders.Anthropic}`]: bedrockAnthropic,
[`${EModelEndpoint.bedrock}-${BedrockProviders.MistralAI}`]: bedrockMistral,
[`${EModelEndpoint.bedrock}-${BedrockProviders.Cohere}`]: bedrockCohere,
[`${EModelEndpoint.bedrock}-${BedrockProviders.Meta}`]: bedrockGeneral,
[`${EModelEndpoint.bedrock}-${BedrockProviders.AI21}`]: bedrockGeneral,
[`${EModelEndpoint.bedrock}-${BedrockProviders.Amazon}`]: bedrockGeneral,
[`${EModelEndpoint.bedrock}-${BedrockProviders.DeepSeek}`]: bedrockGeneral,
[EModelEndpoint.google]: googleConfig,
};
const openAIColumns = {
col1: openAICol1,
col2: openAICol2,
};
const bedrockGeneralColumns = {
col1: bedrockGeneralCol1,
col2: bedrockGeneralCol2,
};
export const presetSettings: Record<
string,
| {
col1: SettingsConfiguration;
col2: SettingsConfiguration;
}
| undefined
> = {
[EModelEndpoint.openAI]: openAIColumns,
[EModelEndpoint.azureOpenAI]: openAIColumns,
[EModelEndpoint.custom]: openAIColumns,
[EModelEndpoint.anthropic]: {
col1: anthropicCol1,
col2: anthropicCol2,
},
[`${EModelEndpoint.bedrock}-${BedrockProviders.Anthropic}`]: {
col1: bedrockAnthropicCol1,
col2: bedrockAnthropicCol2,
},
[`${EModelEndpoint.bedrock}-${BedrockProviders.MistralAI}`]: {
col1: bedrockMistralCol1,
col2: bedrockMistralCol2,
},
[`${EModelEndpoint.bedrock}-${BedrockProviders.Cohere}`]: {
col1: bedrockCohereCol1,
col2: bedrockCohereCol2,
},
[`${EModelEndpoint.bedrock}-${BedrockProviders.Meta}`]: bedrockGeneralColumns,
[`${EModelEndpoint.bedrock}-${BedrockProviders.AI21}`]: bedrockGeneralColumns,
[`${EModelEndpoint.bedrock}-${BedrockProviders.Amazon}`]: bedrockGeneralColumns,
[`${EModelEndpoint.bedrock}-${BedrockProviders.DeepSeek}`]: bedrockGeneralColumns,
[EModelEndpoint.google]: {
col1: googleCol1,
col2: googleCol2,
},
};
export const agentParamSettings: Record<string, SettingsConfiguration | undefined> = Object.entries(
presetSettings,
).reduce<Record<string, SettingsConfiguration | undefined>>((acc, [key, value]) => {
if (value) {
acc[key] = value.col2;
}
return acc;
}, {});

View file

@ -28,6 +28,10 @@ export enum PermissionTypes {
* Type for using the "Run Code" LC Code Interpreter API feature
*/
RUN_CODE = 'RUN_CODE',
/**
* Type for using the "Web Search" feature
*/
WEB_SEARCH = 'WEB_SEARCH',
}
/**
@ -79,6 +83,11 @@ export const runCodePermissionsSchema = z.object({
});
export type TRunCodePermissions = z.infer<typeof runCodePermissionsSchema>;
export const webSearchPermissionsSchema = z.object({
[Permissions.USE]: z.boolean().default(true),
});
export type TWebSearchPermissions = z.infer<typeof webSearchPermissionsSchema>;
// Define a single permissions schema that holds all permission types.
export const permissionsSchema = z.object({
[PermissionTypes.PROMPTS]: promptPermissionsSchema,
@ -87,4 +96,5 @@ export const permissionsSchema = z.object({
[PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema,
[PermissionTypes.TEMPORARY_CHAT]: temporaryChatPermissionsSchema,
[PermissionTypes.RUN_CODE]: runCodePermissionsSchema,
[PermissionTypes.WEB_SEARCH]: webSearchPermissionsSchema,
});

View file

@ -6,6 +6,7 @@ import {
agentPermissionsSchema,
promptPermissionsSchema,
runCodePermissionsSchema,
webSearchPermissionsSchema,
bookmarkPermissionsSchema,
multiConvoPermissionsSchema,
temporaryChatPermissionsSchema,
@ -62,6 +63,9 @@ const defaultRolesSchema = z.object({
[PermissionTypes.RUN_CODE]: runCodePermissionsSchema.extend({
[Permissions.USE]: z.boolean().default(true),
}),
[PermissionTypes.WEB_SEARCH]: webSearchPermissionsSchema.extend({
[Permissions.USE]: z.boolean().default(true),
}),
}),
}),
[SystemRoles.USER]: roleSchema.extend({
@ -96,6 +100,9 @@ export const roleDefaults = defaultRolesSchema.parse({
[PermissionTypes.RUN_CODE]: {
[Permissions.USE]: true,
},
[PermissionTypes.WEB_SEARCH]: {
[Permissions.USE]: true,
},
},
},
[SystemRoles.USER]: {
@ -107,6 +114,7 @@ export const roleDefaults = defaultRolesSchema.parse({
[PermissionTypes.MULTI_CONVO]: {},
[PermissionTypes.TEMPORARY_CHAT]: {},
[PermissionTypes.RUN_CODE]: {},
[PermissionTypes.WEB_SEARCH]: {},
},
},
});

View file

@ -1,6 +1,7 @@
import { z } from 'zod';
import { Tools } from './types/assistants';
import type { TMessageContentParts, FunctionTool, FunctionToolCall } from './types/assistants';
import type { SearchResultData } from './types/web';
import type { TEphemeralAgent } from './types';
import type { TFile } from './types/files';
@ -101,7 +102,8 @@ export const isEphemeralAgent = (
}
const hasMCPSelected = (ephemeralAgent?.mcp?.length ?? 0) > 0;
const hasCodeSelected = (ephemeralAgent?.execute_code ?? false) === true;
return hasMCPSelected || hasCodeSelected;
const hasSearchSelected = (ephemeralAgent?.web_search ?? false) === true;
return hasMCPSelected || hasCodeSelected || hasSearchSelected;
};
export const isParamEndpoint = (
@ -177,6 +179,7 @@ export const defaultAgentFormValues = {
recursion_limit: undefined,
[Tools.execute_code]: false,
[Tools.file_search]: false,
[Tools.web_search]: false,
};
export const ImageVisionTool: FunctionTool = {
@ -517,7 +520,13 @@ export const tMessageSchema = z.object({
iconURL: z.string().nullable().optional(),
});
export type TAttachmentMetadata = { messageId: string; toolCallId: string };
export type TAttachmentMetadata = {
type?: Tools;
messageId: string;
toolCallId: string;
[Tools.web_search]?: SearchResultData;
};
export type TAttachment =
| (TFile & TAttachmentMetadata)
| (Pick<TFile, 'filename' | 'filepath' | 'conversationId'> & {
@ -745,6 +754,7 @@ export type TSetOption = (
export type TConversation = z.infer<typeof tConversationSchema> & {
presetOverride?: Partial<TPreset>;
disableParams?: boolean;
};
export const tSharedLinkSchema = z.object({

View file

@ -10,6 +10,7 @@ import type {
TConversationTag,
TBanner,
} from './schemas';
import { SettingDefinition } from './generate';
export type TOpenAIMessage = OpenAI.Chat.ChatCompletionMessageParam;
export * from './schemas';
@ -43,6 +44,7 @@ export type TEndpointOption = {
export type TEphemeralAgent = {
mcp?: string[];
web_search?: boolean;
execute_code?: boolean;
};
@ -78,7 +80,7 @@ export type EventSubmission = Omit<TSubmission, 'initialResponse'> & { initialRe
export type TPluginAction = {
pluginKey: string;
action: 'install' | 'uninstall';
auth?: unknown;
auth?: Partial<Record<string, string>>;
isEntityTool?: boolean;
};
@ -88,7 +90,7 @@ export type TUpdateUserPlugins = {
isEntityTool?: boolean;
pluginKey: string;
action: string;
auth?: unknown;
auth?: Partial<Record<string, string | null>>;
};
// TODO `label` needs to be changed to the proper `TranslationKeys`
@ -268,6 +270,10 @@ export type TConfig = {
disableBuilder?: boolean;
retrievalModels?: string[];
capabilities?: string[];
customParams?: {
defaultParamsEndpoint?: string;
paramDefinitions?: SettingDefinition[];
};
};
export type TEndpointsConfig =
@ -540,3 +546,13 @@ export type TAcceptTermsResponse = {
};
export type TBannerResponse = TBanner | null;
export type TBalanceResponse = {
tokenCredits: number;
// Automatic refill settings
autoRefillEnabled: boolean;
refillIntervalValue?: number;
refillIntervalUnit?: 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months';
lastRefill?: Date;
refillAmount?: number;
};

View file

@ -19,6 +19,7 @@ export enum Tools {
execute_code = 'execute_code',
code_interpreter = 'code_interpreter',
file_search = 'file_search',
web_search = 'web_search',
retrieval = 'retrieval',
function = 'function',
}
@ -222,6 +223,7 @@ export type Agent = {
hide_sequential_outputs?: boolean;
artifacts?: ArtifactModes;
recursion_limit?: number;
version?: number;
};
export type TAgentsMap = Record<string, Agent | undefined>;

View file

@ -131,6 +131,7 @@ export type BatchFile = {
filepath: string;
embedded: boolean;
source: FileSources;
temp_file_id?: string;
};
export type DeleteFilesBody = {

View file

@ -129,7 +129,20 @@ export type UpdateAgentVariables = {
data: AgentUpdateParams;
};
export type UpdateAgentMutationOptions = MutationOptions<Agent, UpdateAgentVariables>;
export type DuplicateVersionError = Error & {
statusCode?: number;
details?: {
duplicateVersion?: unknown;
versionIndex?: number;
};
};
export type UpdateAgentMutationOptions = MutationOptions<
Agent,
UpdateAgentVariables,
unknown,
DuplicateVersionError
>;
export type DuplicateAgentBody = {
agent_id: string;
@ -159,6 +172,13 @@ export type DeleteAgentActionVariables = {
export type DeleteAgentActionOptions = MutationOptions<void, DeleteAgentActionVariables>;
export type RevertAgentVersionVariables = {
agent_id: string;
version_index: number;
};
export type RevertAgentVersionOptions = MutationOptions<Agent, RevertAgentVersionVariables>;
export type DeleteConversationOptions = MutationOptions<
types.TDeleteConversationResponse,
types.TDeleteConversationRequest

View file

@ -101,7 +101,11 @@ export type AllPromptGroupsResponse = t.TPromptGroup[];
export type ConversationTagsResponse = s.TConversationTag[];
export type VerifyToolAuthParams = { toolId: string };
export type VerifyToolAuthResponse = { authenticated: boolean; message?: string | s.AuthType };
export type VerifyToolAuthResponse = {
authenticated: boolean;
message?: string | s.AuthType;
authTypes?: [string, s.AuthType][];
};
export type GetToolCallParams = { conversationId: string };
export type ToolCallResults = a.ToolCallResult[];

View file

@ -0,0 +1,593 @@
import type { Logger as WinstonLogger } from 'winston';
import type { RunnableConfig } from '@langchain/core/runnables';
export type SearchRefType = 'search' | 'image' | 'news' | 'video' | 'ref';
export enum DATE_RANGE {
PAST_HOUR = 'h',
PAST_24_HOURS = 'd',
PAST_WEEK = 'w',
PAST_MONTH = 'm',
PAST_YEAR = 'y',
}
export type SearchProvider = 'serper' | 'searxng';
export type RerankerType = 'infinity' | 'jina' | 'cohere' | 'none';
export interface Highlight {
score: number;
text: string;
references?: UsedReferences;
}
export type ProcessedSource = {
content?: string;
attribution?: string;
references?: References;
highlights?: Highlight[];
processed?: boolean;
};
export type ProcessedOrganic = OrganicResult & ProcessedSource;
export type ProcessedTopStory = TopStoryResult & ProcessedSource;
export type ValidSource = ProcessedOrganic | ProcessedTopStory;
export type ResultReference = {
link: string;
type: 'link' | 'image' | 'video';
title?: string;
attribution?: string;
};
export interface SearchResultData {
turn?: number;
organic?: ProcessedOrganic[];
topStories?: ProcessedTopStory[];
images?: ImageResult[];
videos?: VideoResult[];
places?: PlaceResult[];
news?: NewsResult[];
shopping?: ShoppingResult[];
knowledgeGraph?: KnowledgeGraphResult;
answerBox?: AnswerBoxResult;
peopleAlsoAsk?: PeopleAlsoAskResult[];
relatedSearches?: Array<{ query: string }>;
references?: ResultReference[];
error?: string;
}
export interface SearchResult {
data?: SearchResultData;
error?: string;
success: boolean;
}
export interface Source {
link: string;
html?: string;
title?: string;
snippet?: string;
date?: string;
}
export interface SearchConfig {
searchProvider?: SearchProvider;
serperApiKey?: string;
searxngInstanceUrl?: string;
searxngApiKey?: string;
}
export type References = {
links: MediaReference[];
images: MediaReference[];
videos: MediaReference[];
};
export interface ScrapeResult {
url: string;
error?: boolean;
content: string;
attribution?: string;
references?: References;
highlights?: Highlight[];
}
export interface ProcessSourcesConfig {
topResults?: number;
strategies?: string[];
filterContent?: boolean;
reranker?: unknown;
logger?: Logger;
}
export interface FirecrawlConfig {
firecrawlApiKey?: string;
firecrawlApiUrl?: string;
firecrawlFormats?: string[];
}
export interface ScraperContentResult {
content: string;
}
export interface ScraperExtractionResult {
no_extraction: ScraperContentResult;
}
export interface JinaRerankerResult {
index: number;
relevance_score: number;
document?: string | { text: string };
}
export interface JinaRerankerResponse {
model: string;
usage: {
total_tokens: number;
};
results: JinaRerankerResult[];
}
export interface CohereRerankerResult {
index: number;
relevance_score: number;
}
export interface CohereRerankerResponse {
results: CohereRerankerResult[];
id: string;
meta: {
api_version: {
version: string;
is_experimental: boolean;
};
billed_units: {
search_units: number;
};
};
}
export type SafeSearchLevel = 0 | 1 | 2;
export type Logger = WinstonLogger;
export interface SearchToolConfig extends SearchConfig, ProcessSourcesConfig, FirecrawlConfig {
logger?: Logger;
safeSearch?: SafeSearchLevel;
jinaApiKey?: string;
cohereApiKey?: string;
rerankerType?: RerankerType;
onSearchResults?: (results: SearchResult, runnableConfig?: RunnableConfig) => void;
onGetHighlights?: (link: string) => void;
}
export interface MediaReference {
originalUrl: string;
title?: string;
text?: string;
}
export type UsedReferences = {
type: 'link' | 'image' | 'video';
originalIndex: number;
reference: MediaReference;
}[];
/** Firecrawl */
export interface FirecrawlScrapeOptions {
formats?: string[];
includeTags?: string[];
excludeTags?: string[];
headers?: Record<string, string>;
waitFor?: number;
timeout?: number;
}
export interface ScrapeMetadata {
// Core source information
sourceURL?: string;
url?: string;
scrapeId?: string;
statusCode?: number;
// Basic metadata
title?: string;
description?: string;
language?: string;
favicon?: string;
viewport?: string;
robots?: string;
'theme-color'?: string;
// Open Graph metadata
'og:url'?: string;
'og:title'?: string;
'og:description'?: string;
'og:type'?: string;
'og:image'?: string;
'og:image:width'?: string;
'og:image:height'?: string;
'og:site_name'?: string;
ogUrl?: string;
ogTitle?: string;
ogDescription?: string;
ogImage?: string;
ogSiteName?: string;
// Article metadata
'article:author'?: string;
'article:published_time'?: string;
'article:modified_time'?: string;
'article:section'?: string;
'article:tag'?: string;
'article:publisher'?: string;
publishedTime?: string;
modifiedTime?: string;
// Twitter metadata
'twitter:site'?: string | boolean | number | null;
'twitter:creator'?: string;
'twitter:card'?: string;
'twitter:image'?: string;
'twitter:dnt'?: string;
'twitter:app:name:iphone'?: string;
'twitter:app:id:iphone'?: string;
'twitter:app:url:iphone'?: string;
'twitter:app:name:ipad'?: string;
'twitter:app:id:ipad'?: string;
'twitter:app:url:ipad'?: string;
'twitter:app:name:googleplay'?: string;
'twitter:app:id:googleplay'?: string;
'twitter:app:url:googleplay'?: string;
// Facebook metadata
'fb:app_id'?: string;
// App links
'al:ios:url'?: string;
'al:ios:app_name'?: string;
'al:ios:app_store_id'?: string;
// Allow for additional properties that might be present
[key: string]: string | number | boolean | null | undefined;
}
export interface FirecrawlScrapeResponse {
success: boolean;
data?: {
markdown?: string;
html?: string;
rawHtml?: string;
screenshot?: string;
links?: string[];
metadata?: ScrapeMetadata;
};
error?: string;
}
export interface FirecrawlScraperConfig {
apiKey?: string;
apiUrl?: string;
formats?: string[];
timeout?: number;
logger?: Logger;
}
export type GetSourcesParams = {
query: string;
date?: DATE_RANGE;
country?: string;
numResults?: number;
safeSearch?: SearchToolConfig['safeSearch'];
images?: boolean;
videos?: boolean;
news?: boolean;
type?: 'search' | 'images' | 'videos' | 'news';
};
/** Serper API */
export interface VideoResult {
title?: string;
link?: string;
snippet?: string;
imageUrl?: string;
duration?: string;
source?: string;
channel?: string;
date?: string;
position?: number;
}
export interface PlaceResult {
position?: number;
name?: string;
address?: string;
latitude?: number;
longitude?: number;
rating?: number;
ratingCount?: number;
category?: string;
identifier?: string;
}
export interface NewsResult {
title?: string;
link?: string;
snippet?: string;
date?: string;
source?: string;
imageUrl?: string;
position?: number;
}
export interface ShoppingResult {
title?: string;
source?: string;
link?: string;
price?: string;
delivery?: string;
imageUrl?: string;
rating?: number;
ratingCount?: number;
offers?: string;
productId?: string;
position?: number;
}
export interface ScholarResult {
title?: string;
link?: string;
publicationInfo?: string;
snippet?: string;
year?: number;
citedBy?: number;
}
export interface ImageResult {
title?: string;
imageUrl?: string;
imageWidth?: number;
imageHeight?: number;
thumbnailUrl?: string;
thumbnailWidth?: number;
thumbnailHeight?: number;
source?: string;
domain?: string;
link?: string;
googleUrl?: string;
position?: number;
}
export interface SerperSearchPayload extends SerperSearchInput {
/**
* Search type/vertical
* Options: "search" (web), "images", "news", "places", "videos"
*/
type?: 'search' | 'images' | 'news' | 'places' | 'videos';
/**
* Starting index for search results pagination (used instead of page)
*/
start?: number;
/**
* Filtering for safe search
* Options: "off", "moderate", "active"
*/
safe?: 'off' | 'moderate' | 'active';
}
export type SerperSearchParameters = Pick<SerperSearchPayload, 'q' | 'type'> & {
engine: 'google';
};
export interface OrganicResult {
position?: number;
title?: string;
link: string;
snippet?: string;
date?: string;
sitelinks?: Array<{
title: string;
link: string;
}>;
}
export interface TopStoryResult {
title?: string;
link: string;
source?: string;
date?: string;
imageUrl?: string;
}
export interface KnowledgeGraphResult {
title?: string;
type?: string;
imageUrl?: string;
description?: string;
descriptionSource?: string;
descriptionLink?: string;
attributes?: Record<string, string>;
website?: string;
}
export interface AnswerBoxResult {
title?: string;
snippet?: string;
snippetHighlighted?: string[];
link?: string;
date?: string;
}
export interface PeopleAlsoAskResult {
question?: string;
snippet?: string;
title?: string;
link?: string;
}
export type RelatedSearches = Array<{ query: string }>;
export interface SerperSearchInput {
/**
* The search query string
*/
q: string;
/**
* Country code for localized results
* Examples: "us", "uk", "ca", "de", etc.
*/
gl?: string;
/**
* Interface language
* Examples: "en", "fr", "de", etc.
*/
hl?: string;
/**
* Number of results to return (up to 100)
*/
num?: number;
/**
* Specific location for contextual results
* Example: "New York, NY"
*/
location?: string;
/**
* Search autocorrection setting
*/
autocorrect?: boolean;
page?: number;
/**
* Date range for search results
* Options: "h" (past hour), "d" (past 24 hours), "w" (past week),
* "m" (past month), "y" (past year)
* `qdr:${DATE_RANGE}`
*/
tbs?: string;
}
export type SerperResultData = {
searchParameters: SerperSearchPayload;
organic?: OrganicResult[];
topStories?: TopStoryResult[];
images?: ImageResult[];
videos?: VideoResult[];
places?: PlaceResult[];
news?: NewsResult[];
shopping?: ShoppingResult[];
peopleAlsoAsk?: PeopleAlsoAskResult[];
relatedSearches?: RelatedSearches;
knowledgeGraph?: KnowledgeGraphResult;
answerBox?: AnswerBoxResult;
credits?: number;
};
/** SearXNG */
export interface SearxNGSearchPayload {
/**
* The search query string
* Supports syntax specific to different search engines
* Example: "site:github.com SearXNG"
*/
q: string;
/**
* Comma-separated list of search categories
* Example: "general,images,news"
*/
categories?: string;
/**
* Comma-separated list of search engines to use
* Example: "google,bing,duckduckgo"
*/
engines?: string;
/**
* Code of the language for search results
* Example: "en", "fr", "de", "es"
*/
language?: string;
/**
* Search page number
* Default: 1
*/
pageno?: number;
/**
* Time range filter for search results
* Options: "day", "month", "year"
*/
time_range?: 'day' | 'month' | 'year';
/**
* Output format of results
* Options: "json", "csv", "rss"
*/
format?: 'json' | 'csv' | 'rss';
/**
* Open search results on new tab
* Options: `0` (off), `1` (on)
*/
results_on_new_tab?: 0 | 1;
/**
* Proxy image results through SearxNG
* Options: true, false
*/
image_proxy?: boolean;
/**
* Service for autocomplete suggestions
* Options: "google", "dbpedia", "duckduckgo", "mwmbl",
* "startpage", "wikipedia", "stract", "swisscows", "qwant"
*/
autocomplete?: string;
/**
* Safe search filtering level
* Options: "0" (off), "1" (moderate), "2" (strict)
*/
safesearch?: 0 | 1 | 2;
/**
* Theme to use for results page
* Default: "simple" (other themes may be available per instance)
*/
theme?: string;
/**
* List of enabled plugins
* Default: "Hash_plugin,Self_Information,Tracker_URL_remover,Ahmia_blacklist"
*/
enabled_plugins?: string;
/**
* List of disabled plugins
*/
disabled_plugins?: string;
/**
* List of enabled engines
*/
enabled_engines?: string;
/**
* List of disabled engines
*/
disabled_engines?: string;
}
export interface SearXNGResult {
title?: string;
url?: string;
content?: string;
publishedDate?: string;
img_src?: string;
}
export type ProcessSourcesFields = {
result: SearchResult;
numElements: number;
query: string;
news: boolean;
proMode: boolean;
onGetHighlights: SearchToolConfig['onGetHighlights'];
};

View file

@ -1,5 +1,15 @@
export const envVarRegex = /^\${(.+)}$/;
/** Extracts the environment variable name from a template literal string */
export function extractVariableName(value: string): string | null {
if (!value) {
return null;
}
const match = value.trim().match(envVarRegex);
return match ? match[1] : null;
}
/** Extracts the value of an environment variable from a string. */
export function extractEnvVariable(value: string) {
if (!value) {

View file

@ -0,0 +1,271 @@
import type {
ScraperTypes,
RerankerTypes,
TCustomConfig,
SearchProviders,
TWebSearchConfig,
} from './config';
import { extractVariableName } from './utils';
import { SearchCategories, SafeSearchTypes } from './config';
import { AuthType } from './schemas';
export function loadWebSearchConfig(
config: TCustomConfig['webSearch'],
): TCustomConfig['webSearch'] {
const serperApiKey = config?.serperApiKey ?? '${SERPER_API_KEY}';
const firecrawlApiKey = config?.firecrawlApiKey ?? '${FIRECRAWL_API_KEY}';
const firecrawlApiUrl = config?.firecrawlApiUrl ?? '${FIRECRAWL_API_URL}';
const jinaApiKey = config?.jinaApiKey ?? '${JINA_API_KEY}';
const cohereApiKey = config?.cohereApiKey ?? '${COHERE_API_KEY}';
const safeSearch = config?.safeSearch ?? SafeSearchTypes.MODERATE;
return {
...config,
safeSearch,
jinaApiKey,
cohereApiKey,
serperApiKey,
firecrawlApiKey,
firecrawlApiUrl,
};
}
export type TWebSearchKeys =
| 'serperApiKey'
| 'firecrawlApiKey'
| 'firecrawlApiUrl'
| 'jinaApiKey'
| 'cohereApiKey';
export type TWebSearchCategories =
| SearchCategories.PROVIDERS
| SearchCategories.SCRAPERS
| SearchCategories.RERANKERS;
export const webSearchAuth = {
providers: {
serper: {
serperApiKey: 1 as const,
},
},
scrapers: {
firecrawl: {
firecrawlApiKey: 1 as const,
/** Optional (0) */
firecrawlApiUrl: 0 as const,
},
},
rerankers: {
jina: { jinaApiKey: 1 as const },
cohere: { cohereApiKey: 1 as const },
},
};
/**
* Extracts all API keys from the webSearchAuth configuration object
*/
export const webSearchKeys: TWebSearchKeys[] = [];
// 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];
// Extract the API keys from the service
for (const key of Object.keys(serviceObj)) {
webSearchKeys.push(key as TWebSearchKeys);
}
}
}
export function extractWebSearchEnvVars({
keys,
config,
}: {
keys: TWebSearchKeys[];
config: TCustomConfig['webSearch'] | undefined;
}): string[] {
if (!config) {
return [];
}
const authFields: string[] = [];
const relevantKeys = keys.filter((k) => k in config);
for (const key of relevantKeys) {
const value = config[key];
if (typeof value === 'string') {
const varName = extractVariableName(value);
if (varName) {
authFields.push(varName);
}
}
}
return authFields;
}
/**
* Type for web search authentication result
*/
export interface WebSearchAuthResult {
/** Whether all required categories have at least one authenticated service */
authenticated: boolean;
/** Authentication type (user_provided or system_defined) by category */
authTypes: [TWebSearchCategories, AuthType][];
/** Original authentication values mapped to their respective keys */
authResult: Partial<TWebSearchConfig>;
}
/**
* Loads and verifies web search authentication values
* @param params - Authentication parameters
* @returns Authentication result
*/
export async function loadWebSearchAuth({
userId,
webSearchConfig,
loadAuthValues,
throwError = true,
}: {
userId: string;
webSearchConfig: TCustomConfig['webSearch'];
loadAuthValues: (params: {
userId: string;
authFields: string[];
optional?: Set<string>;
throwError?: boolean;
}) => Promise<Record<string, string>>;
throwError?: boolean;
}): Promise<WebSearchAuthResult> {
let authenticated = true;
const authResult: Partial<TWebSearchConfig> = {};
/** Type-safe iterator for the category-service combinations */
async function checkAuth<C extends TWebSearchCategories>(
category: C,
): Promise<[boolean, boolean]> {
type ServiceType = keyof (typeof webSearchAuth)[C];
let isUserProvided = false;
// Check if a specific service is specified in the config
let specificService: ServiceType | undefined;
if (category === SearchCategories.PROVIDERS && webSearchConfig?.searchProvider) {
specificService = webSearchConfig.searchProvider as unknown as ServiceType;
} else if (category === SearchCategories.SCRAPERS && webSearchConfig?.scraperType) {
specificService = webSearchConfig.scraperType as unknown as ServiceType;
} else if (category === SearchCategories.RERANKERS && webSearchConfig?.rerankerType) {
specificService = webSearchConfig.rerankerType as unknown as ServiceType;
}
// If a specific service is specified, only check that one
const services = specificService
? [specificService]
: (Object.keys(webSearchAuth[category]) as ServiceType[]);
for (const service of services) {
// Skip if the service doesn't exist in the webSearchAuth config
if (!webSearchAuth[category][service]) {
continue;
}
const serviceConfig = webSearchAuth[category][service];
// Split keys into required and optional
const requiredKeys: TWebSearchKeys[] = [];
const optionalKeys: TWebSearchKeys[] = [];
for (const key in serviceConfig) {
const typedKey = key as TWebSearchKeys;
if (serviceConfig[typedKey as keyof typeof serviceConfig] === 1) {
requiredKeys.push(typedKey);
} else if (serviceConfig[typedKey as keyof typeof serviceConfig] === 0) {
optionalKeys.push(typedKey);
}
}
if (requiredKeys.length === 0) continue;
const requiredAuthFields = extractWebSearchEnvVars({
keys: requiredKeys,
config: webSearchConfig,
});
const optionalAuthFields = extractWebSearchEnvVars({
keys: optionalKeys,
config: webSearchConfig,
});
if (requiredAuthFields.length !== requiredKeys.length) continue;
const allKeys = [...requiredKeys, ...optionalKeys];
const allAuthFields = [...requiredAuthFields, ...optionalAuthFields];
const optionalSet = new Set(optionalAuthFields);
try {
const authValues = await loadAuthValues({
userId,
authFields: allAuthFields,
optional: optionalSet,
throwError,
});
let allFieldsAuthenticated = true;
for (let j = 0; j < allAuthFields.length; j++) {
const field = allAuthFields[j];
const value = authValues[field];
const originalKey = allKeys[j];
if (originalKey) authResult[originalKey] = value;
if (!optionalSet.has(field) && !value) {
allFieldsAuthenticated = false;
break;
}
if (!isUserProvided && process.env[field] !== value) {
isUserProvided = true;
}
}
if (!allFieldsAuthenticated) {
continue;
}
if (category === SearchCategories.PROVIDERS) {
authResult.searchProvider = service as SearchProviders;
} else if (category === SearchCategories.SCRAPERS) {
authResult.scraperType = service as ScraperTypes;
} else if (category === SearchCategories.RERANKERS) {
authResult.rerankerType = service as RerankerTypes;
}
return [true, isUserProvided];
} catch {
continue;
}
}
return [false, isUserProvided];
}
const categories = [
SearchCategories.PROVIDERS,
SearchCategories.SCRAPERS,
SearchCategories.RERANKERS,
] as const;
const authTypes: [TWebSearchCategories, AuthType][] = [];
for (const category of categories) {
const [isCategoryAuthenticated, isUserProvided] = await checkAuth(category);
if (!isCategoryAuthenticated) {
authenticated = false;
authTypes.push([category, AuthType.USER_PROVIDED]);
continue;
}
authTypes.push([category, isUserProvided ? AuthType.USER_PROVIDED : AuthType.SYSTEM_DEFINED]);
}
authResult.safeSearch = webSearchConfig?.safeSearch ?? SafeSearchTypes.MODERATE;
authResult.scraperTimeout = webSearchConfig?.scraperTimeout ?? 7500;
return {
authTypes,
authResult,
authenticated,
};
}