mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 00:40:14 +01:00
🧩 feat: Support Alternate API Keys for Plugins (#1760)
* refactor(DALL-E): retrieve env variables at runtime and not from memory * feat(plugins): add alternate env variable handling to allow setting one api key for multiple plugins * docs: update docs
This commit is contained in:
parent
927ce5395b
commit
39caeb2027
10 changed files with 328 additions and 113 deletions
|
|
@ -8,15 +8,6 @@ const { processFileURL } = require('~/server/services/Files/process');
|
||||||
const extractBaseURL = require('~/utils/extractBaseURL');
|
const extractBaseURL = require('~/utils/extractBaseURL');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const {
|
|
||||||
DALLE2_SYSTEM_PROMPT,
|
|
||||||
DALLE_REVERSE_PROXY,
|
|
||||||
PROXY,
|
|
||||||
DALLE2_AZURE_API_VERSION,
|
|
||||||
DALLE2_BASEURL,
|
|
||||||
DALLE2_API_KEY,
|
|
||||||
DALLE_API_KEY,
|
|
||||||
} = process.env;
|
|
||||||
class OpenAICreateImage extends Tool {
|
class OpenAICreateImage extends Tool {
|
||||||
constructor(fields = {}) {
|
constructor(fields = {}) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -26,19 +17,22 @@ class OpenAICreateImage extends Tool {
|
||||||
let apiKey = fields.DALLE2_API_KEY ?? fields.DALLE_API_KEY ?? this.getApiKey();
|
let apiKey = fields.DALLE2_API_KEY ?? fields.DALLE_API_KEY ?? this.getApiKey();
|
||||||
|
|
||||||
const config = { apiKey };
|
const config = { apiKey };
|
||||||
if (DALLE_REVERSE_PROXY) {
|
if (process.env.DALLE_REVERSE_PROXY) {
|
||||||
config.baseURL = extractBaseURL(DALLE_REVERSE_PROXY);
|
config.baseURL = extractBaseURL(process.env.DALLE_REVERSE_PROXY);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DALLE2_AZURE_API_VERSION && DALLE2_BASEURL) {
|
if (process.env.DALLE2_AZURE_API_VERSION && process.env.DALLE2_BASEURL) {
|
||||||
config.baseURL = DALLE2_BASEURL;
|
config.baseURL = process.env.DALLE2_BASEURL;
|
||||||
config.defaultQuery = { 'api-version': DALLE2_AZURE_API_VERSION };
|
config.defaultQuery = { 'api-version': process.env.DALLE2_AZURE_API_VERSION };
|
||||||
config.defaultHeaders = { 'api-key': DALLE2_API_KEY, 'Content-Type': 'application/json' };
|
config.defaultHeaders = {
|
||||||
config.apiKey = DALLE2_API_KEY;
|
'api-key': process.env.DALLE2_API_KEY,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
config.apiKey = process.env.DALLE2_API_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PROXY) {
|
if (process.env.PROXY) {
|
||||||
config.httpAgent = new HttpsProxyAgent(PROXY);
|
config.httpAgent = new HttpsProxyAgent(process.env.PROXY);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.openai = new OpenAI(config);
|
this.openai = new OpenAI(config);
|
||||||
|
|
@ -51,7 +45,7 @@ Guidelines:
|
||||||
"Subject: [subject], Style: [style], Color: [color], Details: [details], Emotion: [emotion]"
|
"Subject: [subject], Style: [style], Color: [color], Details: [details], Emotion: [emotion]"
|
||||||
- Generate images only once per human query unless explicitly requested by the user`;
|
- Generate images only once per human query unless explicitly requested by the user`;
|
||||||
this.description_for_model =
|
this.description_for_model =
|
||||||
DALLE2_SYSTEM_PROMPT ??
|
process.env.DALLE2_SYSTEM_PROMPT ??
|
||||||
`// Whenever a description of an image is given, generate prompts (following these rules), and use dalle to create the image. If the user does not ask for a specific number of images, default to creating 2 prompts to send to dalle that are written to be as diverse as possible. All prompts sent to dalle must abide by the following policies:
|
`// Whenever a description of an image is given, generate prompts (following these rules), and use dalle to create the image. If the user does not ask for a specific number of images, default to creating 2 prompts to send to dalle that are written to be as diverse as possible. All prompts sent to dalle must abide by the following policies:
|
||||||
// 1. Prompts must be in English. Translate to English if needed.
|
// 1. Prompts must be in English. Translate to English if needed.
|
||||||
// 2. One image per function call. Create only 1 image per request unless explicitly told to generate more than 1 image.
|
// 2. One image per function call. Create only 1 image per request unless explicitly told to generate more than 1 image.
|
||||||
|
|
@ -67,7 +61,7 @@ Guidelines:
|
||||||
}
|
}
|
||||||
|
|
||||||
getApiKey() {
|
getApiKey() {
|
||||||
const apiKey = DALLE2_API_KEY ?? DALLE_API_KEY ?? '';
|
const apiKey = process.env.DALLE2_API_KEY ?? process.env.DALLE_API_KEY ?? '';
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error('Missing DALLE_API_KEY environment variable.');
|
throw new Error('Missing DALLE_API_KEY environment variable.');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@
|
||||||
"icon": "https://i.imgur.com/u2TzXzH.png",
|
"icon": "https://i.imgur.com/u2TzXzH.png",
|
||||||
"authConfig": [
|
"authConfig": [
|
||||||
{
|
{
|
||||||
"authField": "DALLE2_API_KEY",
|
"authField": "DALLE2_API_KEY||DALLE_API_KEY",
|
||||||
"label": "OpenAI API Key",
|
"label": "OpenAI API Key",
|
||||||
"description": "You can use DALL-E with your API Key from OpenAI."
|
"description": "You can use DALL-E with your API Key from OpenAI."
|
||||||
}
|
}
|
||||||
|
|
@ -102,7 +102,7 @@
|
||||||
"icon": "https://i.imgur.com/u2TzXzH.png",
|
"icon": "https://i.imgur.com/u2TzXzH.png",
|
||||||
"authConfig": [
|
"authConfig": [
|
||||||
{
|
{
|
||||||
"authField": "DALLE3_API_KEY",
|
"authField": "DALLE3_API_KEY||DALLE_API_KEY",
|
||||||
"label": "OpenAI API Key",
|
"label": "OpenAI API Key",
|
||||||
"description": "You can use DALL-E with your API Key from OpenAI."
|
"description": "You can use DALL-E with your API Key from OpenAI."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,6 @@ const { processFileURL } = require('~/server/services/Files/process');
|
||||||
const extractBaseURL = require('~/utils/extractBaseURL');
|
const extractBaseURL = require('~/utils/extractBaseURL');
|
||||||
const { logger } = require('~/config');
|
const { logger } = require('~/config');
|
||||||
|
|
||||||
const {
|
|
||||||
DALLE3_SYSTEM_PROMPT,
|
|
||||||
DALLE_REVERSE_PROXY,
|
|
||||||
PROXY,
|
|
||||||
DALLE3_AZURE_API_VERSION,
|
|
||||||
DALLE3_BASEURL,
|
|
||||||
DALLE3_API_KEY,
|
|
||||||
} = process.env;
|
|
||||||
class DALLE3 extends Tool {
|
class DALLE3 extends Tool {
|
||||||
constructor(fields = {}) {
|
constructor(fields = {}) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -25,19 +17,22 @@ class DALLE3 extends Tool {
|
||||||
this.fileStrategy = fields.fileStrategy;
|
this.fileStrategy = fields.fileStrategy;
|
||||||
let apiKey = fields.DALLE3_API_KEY ?? fields.DALLE_API_KEY ?? this.getApiKey();
|
let apiKey = fields.DALLE3_API_KEY ?? fields.DALLE_API_KEY ?? this.getApiKey();
|
||||||
const config = { apiKey };
|
const config = { apiKey };
|
||||||
if (DALLE_REVERSE_PROXY) {
|
if (process.env.DALLE_REVERSE_PROXY) {
|
||||||
config.baseURL = extractBaseURL(DALLE_REVERSE_PROXY);
|
config.baseURL = extractBaseURL(process.env.DALLE_REVERSE_PROXY);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DALLE3_AZURE_API_VERSION && DALLE3_BASEURL) {
|
if (process.env.DALLE3_AZURE_API_VERSION && process.env.DALLE3_BASEURL) {
|
||||||
config.baseURL = DALLE3_BASEURL;
|
config.baseURL = process.env.DALLE3_BASEURL;
|
||||||
config.defaultQuery = { 'api-version': DALLE3_AZURE_API_VERSION };
|
config.defaultQuery = { 'api-version': process.env.DALLE3_AZURE_API_VERSION };
|
||||||
config.defaultHeaders = { 'api-key': DALLE3_API_KEY, 'Content-Type': 'application/json' };
|
config.defaultHeaders = {
|
||||||
config.apiKey = DALLE3_API_KEY;
|
'api-key': process.env.DALLE3_API_KEY,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
config.apiKey = process.env.DALLE3_API_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PROXY) {
|
if (process.env.PROXY) {
|
||||||
config.httpAgent = new HttpsProxyAgent(PROXY);
|
config.httpAgent = new HttpsProxyAgent(process.env.PROXY);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.openai = new OpenAI(config);
|
this.openai = new OpenAI(config);
|
||||||
|
|
@ -47,7 +42,7 @@ class DALLE3 extends Tool {
|
||||||
- Create only one image, without repeating or listing descriptions outside the "prompts" field.
|
- Create only one image, without repeating or listing descriptions outside the "prompts" field.
|
||||||
- Maintains the original intent of the description, with parameters for image style, quality, and size to tailor the output.`;
|
- Maintains the original intent of the description, with parameters for image style, quality, and size to tailor the output.`;
|
||||||
this.description_for_model =
|
this.description_for_model =
|
||||||
DALLE3_SYSTEM_PROMPT ??
|
process.env.DALLE3_SYSTEM_PROMPT ??
|
||||||
`// Whenever a description of an image is given, generate prompts (following these rules), and use dalle to create the image. If the user does not ask for a specific number of images, default to creating 2 prompts to send to dalle that are written to be as diverse as possible. All prompts sent to dalle must abide by the following policies:
|
`// Whenever a description of an image is given, generate prompts (following these rules), and use dalle to create the image. If the user does not ask for a specific number of images, default to creating 2 prompts to send to dalle that are written to be as diverse as possible. All prompts sent to dalle must abide by the following policies:
|
||||||
// 1. Prompts must be in English. Translate to English if needed.
|
// 1. Prompts must be in English. Translate to English if needed.
|
||||||
// 2. One image per function call. Create only 1 image per request unless explicitly told to generate more than 1 image.
|
// 2. One image per function call. Create only 1 image per request unless explicitly told to generate more than 1 image.
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,14 @@ const getOpenAIKey = async (options, user) => {
|
||||||
return openAIApiKey || (await getUserPluginAuthValue(user, 'OPENAI_API_KEY'));
|
return openAIApiKey || (await getUserPluginAuthValue(user, 'OPENAI_API_KEY'));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the availability and authentication of tools for a user based on environment variables or user-specific plugin authentication values.
|
||||||
|
* Tools without required authentication or with valid authentication are considered valid.
|
||||||
|
*
|
||||||
|
* @param {Object} user The user object for whom to validate tool access.
|
||||||
|
* @param {Array<string>} tools An array of tool identifiers to validate. Defaults to an empty array.
|
||||||
|
* @returns {Promise<Array<string>>} A promise that resolves to an array of valid tool identifiers.
|
||||||
|
*/
|
||||||
const validateTools = async (user, tools = []) => {
|
const validateTools = async (user, tools = []) => {
|
||||||
try {
|
try {
|
||||||
const validToolsSet = new Set(tools);
|
const validToolsSet = new Set(tools);
|
||||||
|
|
@ -37,16 +45,34 @@ const validateTools = async (user, tools = []) => {
|
||||||
validToolsSet.has(tool.pluginKey),
|
validToolsSet.has(tool.pluginKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the credentials for a given auth field or set of alternate auth fields for a tool.
|
||||||
|
* If valid admin or user authentication is found, the function returns early. Otherwise, it removes the tool from the set of valid tools.
|
||||||
|
*
|
||||||
|
* @param {string} authField The authentication field or fields (separated by "||" for alternates) to validate.
|
||||||
|
* @param {string} toolName The identifier of the tool being validated.
|
||||||
|
*/
|
||||||
const validateCredentials = async (authField, toolName) => {
|
const validateCredentials = async (authField, toolName) => {
|
||||||
const adminAuth = process.env[authField];
|
const fields = authField.split('||');
|
||||||
if (adminAuth && adminAuth.length > 0) {
|
for (const field of fields) {
|
||||||
return;
|
const adminAuth = process.env[field];
|
||||||
|
if (adminAuth && adminAuth.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let userAuth = null;
|
||||||
|
try {
|
||||||
|
userAuth = await getUserPluginAuthValue(user, field);
|
||||||
|
} catch (err) {
|
||||||
|
if (field === fields[fields.length - 1] && !userAuth) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (userAuth && userAuth.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userAuth = await getUserPluginAuthValue(user, authField);
|
|
||||||
if (userAuth && userAuth.length > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
validToolsSet.delete(toolName);
|
validToolsSet.delete(toolName);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -63,20 +89,55 @@ const validateTools = async (user, tools = []) => {
|
||||||
return Array.from(validToolsSet.values());
|
return Array.from(validToolsSet.values());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('[validateTools] There was a problem validating tools', err);
|
logger.error('[validateTools] There was a problem validating tools', err);
|
||||||
throw new Error(err);
|
throw new Error('There was a problem validating tools');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadToolWithAuth = async (userId, authFields, ToolConstructor, options = {}) => {
|
/**
|
||||||
|
* Initializes a tool with authentication values for the given user, supporting alternate authentication fields.
|
||||||
|
* Authentication fields can have alternates separated by "||", and the first defined variable will be used.
|
||||||
|
*
|
||||||
|
* @param {string} userId The user ID for which the tool is being loaded.
|
||||||
|
* @param {Array<string>} authFields Array of strings representing the authentication fields. Supports alternate fields delimited by "||".
|
||||||
|
* @param {typeof import('langchain/tools').Tool} ToolConstructor The constructor function for the tool to be initialized.
|
||||||
|
* @param {Object} options Optional parameters to be passed to the tool constructor alongside authentication values.
|
||||||
|
* @returns {Function} An Async function that, when called, asynchronously initializes and returns an instance of the tool with authentication.
|
||||||
|
*/
|
||||||
|
const loadToolWithAuth = (userId, authFields, ToolConstructor, options = {}) => {
|
||||||
return async function () {
|
return async function () {
|
||||||
let authValues = {};
|
let authValues = {};
|
||||||
|
|
||||||
for (const authField of authFields) {
|
/**
|
||||||
let authValue = process.env[authField];
|
* Finds the first non-empty value for the given authentication field, supporting alternate fields.
|
||||||
if (!authValue) {
|
* @param {string[]} fields Array of strings representing the authentication fields. Supports alternate fields delimited by "||".
|
||||||
authValue = await getUserPluginAuthValue(userId, authField);
|
* @returns {Promise<{ authField: string, authValue: string} | null>} An object containing the authentication field and value, or null if not found.
|
||||||
|
*/
|
||||||
|
const findAuthValue = async (fields) => {
|
||||||
|
for (const field of fields) {
|
||||||
|
let value = process.env[field];
|
||||||
|
if (value) {
|
||||||
|
return { authField: field, authValue: value };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
value = await getUserPluginAuthValue(userId, field);
|
||||||
|
} catch (err) {
|
||||||
|
if (field === fields[fields.length - 1] && !value) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
return { authField: field, authValue: value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let authField of authFields) {
|
||||||
|
const fields = authField.split('||');
|
||||||
|
const result = await findAuthValue(fields);
|
||||||
|
if (result) {
|
||||||
|
authValues[result.authField] = result.authValue;
|
||||||
}
|
}
|
||||||
authValues[authField] = authValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ToolConstructor({ ...options, ...authValues, userId });
|
return new ToolConstructor({ ...options, ...authValues, userId });
|
||||||
|
|
@ -194,7 +255,7 @@ const loadTools = async ({
|
||||||
|
|
||||||
if (toolConstructors[tool]) {
|
if (toolConstructors[tool]) {
|
||||||
const options = toolOptions[tool] || {};
|
const options = toolOptions[tool] || {};
|
||||||
const toolInstance = await loadToolWithAuth(
|
const toolInstance = loadToolWithAuth(
|
||||||
user,
|
user,
|
||||||
toolAuthFields[tool],
|
toolAuthFields[tool],
|
||||||
toolConstructors[tool],
|
toolConstructors[tool],
|
||||||
|
|
@ -250,6 +311,7 @@ const loadTools = async ({
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
loadToolWithAuth,
|
||||||
validateTools,
|
validateTools,
|
||||||
loadTools,
|
loadTools,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -4,26 +4,33 @@ const mockUser = {
|
||||||
findByIdAndDelete: jest.fn(),
|
findByIdAndDelete: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
var mockPluginService = {
|
const mockPluginService = {
|
||||||
updateUserPluginAuth: jest.fn(),
|
updateUserPluginAuth: jest.fn(),
|
||||||
deleteUserPluginAuth: jest.fn(),
|
deleteUserPluginAuth: jest.fn(),
|
||||||
getUserPluginAuthValue: jest.fn(),
|
getUserPluginAuthValue: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('../../../../models/User', () => {
|
jest.mock('~/models/User', () => {
|
||||||
return function () {
|
return function () {
|
||||||
return mockUser;
|
return mockUser;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('../../../../server/services/PluginService', () => mockPluginService);
|
jest.mock('~/server/services/PluginService', () => mockPluginService);
|
||||||
|
|
||||||
const User = require('../../../../models/User');
|
|
||||||
const { validateTools, loadTools } = require('./');
|
|
||||||
const PluginService = require('../../../../server/services/PluginService');
|
|
||||||
const { BaseChatModel } = require('langchain/chat_models/openai');
|
|
||||||
const { Calculator } = require('langchain/tools/calculator');
|
const { Calculator } = require('langchain/tools/calculator');
|
||||||
const { availableTools, OpenAICreateImage, GoogleSearchAPI, StructuredSD } = require('../');
|
const { BaseChatModel } = require('langchain/chat_models/openai');
|
||||||
|
|
||||||
|
const User = require('~/models/User');
|
||||||
|
const PluginService = require('~/server/services/PluginService');
|
||||||
|
const { validateTools, loadTools, loadToolWithAuth } = require('./handleTools');
|
||||||
|
const {
|
||||||
|
availableTools,
|
||||||
|
OpenAICreateImage,
|
||||||
|
GoogleSearchAPI,
|
||||||
|
StructuredSD,
|
||||||
|
WolframAlphaAPI,
|
||||||
|
} = require('../');
|
||||||
|
|
||||||
describe('Tool Handlers', () => {
|
describe('Tool Handlers', () => {
|
||||||
let fakeUser;
|
let fakeUser;
|
||||||
|
|
@ -44,7 +51,10 @@ describe('Tool Handlers', () => {
|
||||||
});
|
});
|
||||||
mockPluginService.updateUserPluginAuth.mockImplementation(
|
mockPluginService.updateUserPluginAuth.mockImplementation(
|
||||||
(userId, authField, _pluginKey, credential) => {
|
(userId, authField, _pluginKey, credential) => {
|
||||||
userAuthValues[`${userId}-${authField}`] = credential;
|
const fields = authField.split('||');
|
||||||
|
fields.forEach((field) => {
|
||||||
|
userAuthValues[`${userId}-${field}`] = credential;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -134,6 +144,18 @@ describe('Tool Handlers', () => {
|
||||||
loadTool2 = toolFunctions[sampleTools[1]];
|
loadTool2 = toolFunctions[sampleTools[1]];
|
||||||
loadTool3 = toolFunctions[sampleTools[2]];
|
loadTool3 = toolFunctions[sampleTools[2]];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let originalEnv;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalEnv = process.env;
|
||||||
|
process.env = { ...originalEnv };
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = originalEnv;
|
||||||
|
});
|
||||||
|
|
||||||
it('returns the expected load functions for requested tools', async () => {
|
it('returns the expected load functions for requested tools', async () => {
|
||||||
expect(loadTool1).toBeDefined();
|
expect(loadTool1).toBeDefined();
|
||||||
expect(loadTool2).toBeDefined();
|
expect(loadTool2).toBeDefined();
|
||||||
|
|
@ -150,6 +172,86 @@ describe('Tool Handlers', () => {
|
||||||
expect(authTool).toBeInstanceOf(ToolClass);
|
expect(authTool).toBeInstanceOf(ToolClass);
|
||||||
expect(tool).toBeInstanceOf(ToolClass2);
|
expect(tool).toBeInstanceOf(ToolClass2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should initialize an authenticated tool with primary auth field', async () => {
|
||||||
|
process.env.DALLE2_API_KEY = 'mocked_api_key';
|
||||||
|
const initToolFunction = loadToolWithAuth(
|
||||||
|
'userId',
|
||||||
|
['DALLE2_API_KEY||DALLE_API_KEY'],
|
||||||
|
ToolClass,
|
||||||
|
);
|
||||||
|
const authTool = await initToolFunction();
|
||||||
|
|
||||||
|
expect(authTool).toBeInstanceOf(ToolClass);
|
||||||
|
expect(mockPluginService.getUserPluginAuthValue).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize an authenticated tool with alternate auth field when primary is missing', async () => {
|
||||||
|
delete process.env.DALLE2_API_KEY; // Ensure the primary key is not set
|
||||||
|
process.env.DALLE_API_KEY = 'mocked_alternate_api_key';
|
||||||
|
const initToolFunction = loadToolWithAuth(
|
||||||
|
'userId',
|
||||||
|
['DALLE2_API_KEY||DALLE_API_KEY'],
|
||||||
|
ToolClass,
|
||||||
|
);
|
||||||
|
const authTool = await initToolFunction();
|
||||||
|
|
||||||
|
expect(authTool).toBeInstanceOf(ToolClass);
|
||||||
|
expect(mockPluginService.getUserPluginAuthValue).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockPluginService.getUserPluginAuthValue).toHaveBeenCalledWith(
|
||||||
|
'userId',
|
||||||
|
'DALLE2_API_KEY',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fallback to getUserPluginAuthValue when env vars are missing', async () => {
|
||||||
|
mockPluginService.updateUserPluginAuth('userId', 'DALLE_API_KEY', 'dalle', 'mocked_api_key');
|
||||||
|
const initToolFunction = loadToolWithAuth(
|
||||||
|
'userId',
|
||||||
|
['DALLE2_API_KEY||DALLE_API_KEY'],
|
||||||
|
ToolClass,
|
||||||
|
);
|
||||||
|
const authTool = await initToolFunction();
|
||||||
|
|
||||||
|
expect(authTool).toBeInstanceOf(ToolClass);
|
||||||
|
expect(mockPluginService.getUserPluginAuthValue).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize an authenticated tool with singular auth field', async () => {
|
||||||
|
process.env.WOLFRAM_APP_ID = 'mocked_app_id';
|
||||||
|
const initToolFunction = loadToolWithAuth('userId', ['WOLFRAM_APP_ID'], WolframAlphaAPI);
|
||||||
|
const authTool = await initToolFunction();
|
||||||
|
|
||||||
|
expect(authTool).toBeInstanceOf(WolframAlphaAPI);
|
||||||
|
expect(mockPluginService.getUserPluginAuthValue).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize an authenticated tool when env var is set', async () => {
|
||||||
|
process.env.WOLFRAM_APP_ID = 'mocked_app_id';
|
||||||
|
const initToolFunction = loadToolWithAuth('userId', ['WOLFRAM_APP_ID'], WolframAlphaAPI);
|
||||||
|
const authTool = await initToolFunction();
|
||||||
|
|
||||||
|
expect(authTool).toBeInstanceOf(WolframAlphaAPI);
|
||||||
|
expect(mockPluginService.getUserPluginAuthValue).not.toHaveBeenCalledWith(
|
||||||
|
'userId',
|
||||||
|
'WOLFRAM_APP_ID',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fallback to getUserPluginAuthValue when singular env var is missing', async () => {
|
||||||
|
delete process.env.WOLFRAM_APP_ID; // Ensure the environment variable is not set
|
||||||
|
mockPluginService.getUserPluginAuthValue.mockResolvedValue('mocked_user_auth_value');
|
||||||
|
const initToolFunction = loadToolWithAuth('userId', ['WOLFRAM_APP_ID'], WolframAlphaAPI);
|
||||||
|
const authTool = await initToolFunction();
|
||||||
|
|
||||||
|
expect(authTool).toBeInstanceOf(WolframAlphaAPI);
|
||||||
|
expect(mockPluginService.getUserPluginAuthValue).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockPluginService.getUserPluginAuthValue).toHaveBeenCalledWith(
|
||||||
|
'userId',
|
||||||
|
'WOLFRAM_APP_ID',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw an error for an unauthenticated tool', async () => {
|
it('should throw an error for an unauthenticated tool', async () => {
|
||||||
try {
|
try {
|
||||||
await loadTool2();
|
await loadTool2();
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,48 @@
|
||||||
const { getUserPluginAuthValue } = require('../../../../server/services/PluginService');
|
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
|
||||||
const { availableTools } = require('../');
|
const { availableTools } = require('../');
|
||||||
|
|
||||||
const loadToolSuite = async ({ pluginKey, tools, user, options }) => {
|
/**
|
||||||
|
* Loads a suite of tools with authentication values for a given user, supporting alternate authentication fields.
|
||||||
|
* Authentication fields can have alternates separated by "||", and the first defined variable will be used.
|
||||||
|
*
|
||||||
|
* @param {Object} params Parameters for loading the tool suite.
|
||||||
|
* @param {string} params.pluginKey Key identifying the plugin whose tools are to be loaded.
|
||||||
|
* @param {Array<Function>} params.tools Array of tool constructor functions.
|
||||||
|
* @param {Object} params.user User object for whom the tools are being loaded.
|
||||||
|
* @param {Object} [params.options={}] Optional parameters to be passed to each tool constructor.
|
||||||
|
* @returns {Promise<Array>} A promise that resolves to an array of instantiated tools.
|
||||||
|
*/
|
||||||
|
const loadToolSuite = async ({ pluginKey, tools, user, options = {} }) => {
|
||||||
const authConfig = availableTools.find((tool) => tool.pluginKey === pluginKey).authConfig;
|
const authConfig = availableTools.find((tool) => tool.pluginKey === pluginKey).authConfig;
|
||||||
const suite = [];
|
const suite = [];
|
||||||
const authValues = {};
|
const authValues = {};
|
||||||
|
|
||||||
for (const auth of authConfig) {
|
const findAuthValue = async (authField) => {
|
||||||
let authValue = process.env[auth.authField];
|
const fields = authField.split('||');
|
||||||
if (!authValue) {
|
for (const field of fields) {
|
||||||
authValue = await getUserPluginAuthValue(user, auth.authField);
|
let value = process.env[field];
|
||||||
|
if (value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
value = await getUserPluginAuthValue(user, field);
|
||||||
|
if (value) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error fetching plugin auth value for ${field}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auth of authConfig) {
|
||||||
|
const authValue = await findAuthValue(auth.authField);
|
||||||
|
if (authValue !== null) {
|
||||||
|
authValues[auth.authField] = authValue;
|
||||||
|
} else {
|
||||||
|
console.warn(`No auth value found for ${auth.authField}`);
|
||||||
}
|
}
|
||||||
authValues[auth.authField] = authValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const tool of tools) {
|
for (const tool of tools) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@ const { CacheKeys } = require('librechat-data-provider');
|
||||||
const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs');
|
const { addOpenAPISpecs } = require('~/app/clients/tools/util/addOpenAPISpecs');
|
||||||
const { getLogStores } = require('~/cache');
|
const { getLogStores } = require('~/cache');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters out duplicate plugins from the list of plugins.
|
||||||
|
*
|
||||||
|
* @param {TPlugin[]} plugins The list of plugins to filter.
|
||||||
|
* @returns {TPlugin[]} The list of plugins with duplicates removed.
|
||||||
|
*/
|
||||||
const filterUniquePlugins = (plugins) => {
|
const filterUniquePlugins = (plugins) => {
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
return plugins.filter((plugin) => {
|
return plugins.filter((plugin) => {
|
||||||
|
|
@ -13,17 +19,31 @@ const filterUniquePlugins = (plugins) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a plugin is authenticated by checking if all required authentication fields have non-empty values.
|
||||||
|
* Supports alternate authentication fields, allowing validation against multiple possible environment variables.
|
||||||
|
*
|
||||||
|
* @param {TPlugin} plugin The plugin object containing the authentication configuration.
|
||||||
|
* @returns {boolean} True if the plugin is authenticated for all required fields, false otherwise.
|
||||||
|
*/
|
||||||
const isPluginAuthenticated = (plugin) => {
|
const isPluginAuthenticated = (plugin) => {
|
||||||
if (!plugin.authConfig || plugin.authConfig.length === 0) {
|
if (!plugin.authConfig || plugin.authConfig.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return plugin.authConfig.every((authFieldObj) => {
|
return plugin.authConfig.every((authFieldObj) => {
|
||||||
const envValue = process.env[authFieldObj.authField];
|
const authFieldOptions = authFieldObj.authField.split('||');
|
||||||
if (envValue === 'user_provided') {
|
let isFieldAuthenticated = false;
|
||||||
return false;
|
|
||||||
|
for (const fieldOption of authFieldOptions) {
|
||||||
|
const envValue = process.env[fieldOption];
|
||||||
|
if (envValue && envValue.trim() !== '' && envValue !== 'user_provided') {
|
||||||
|
isFieldAuthenticated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return envValue && envValue.trim() !== '';
|
|
||||||
|
return isFieldAuthenticated;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,12 @@
|
||||||
* @memberof typedefs
|
* @memberof typedefs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @exports TPlugin
|
||||||
|
* @typedef {import('librechat-data-provider').TPlugin} TPlugin
|
||||||
|
* @memberof typedefs
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @exports TCustomConfig
|
* @exports TCustomConfig
|
||||||
* @typedef {import('librechat-data-provider').TCustomConfig} TCustomConfig
|
* @typedef {import('librechat-data-provider').TCustomConfig} TCustomConfig
|
||||||
|
|
|
||||||
|
|
@ -26,44 +26,47 @@ function PluginAuthForm({ plugin, onSubmit }: TPluginAuthFormProps) {
|
||||||
onSubmit({ pluginKey: plugin?.pluginKey ?? '', action: 'install', auth }),
|
onSubmit({ pluginKey: plugin?.pluginKey ?? '', action: 'install', auth }),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{plugin?.authConfig?.map((config: TPluginAuthConfig, i: number) => (
|
{plugin?.authConfig?.map((config: TPluginAuthConfig, i: number) => {
|
||||||
<div key={`${config.authField}-${i}`} className="flex w-full flex-col gap-1">
|
const authField = config.authField.split('||')[0];
|
||||||
<label
|
return (
|
||||||
htmlFor={config.authField}
|
<div key={`${authField}-${i}`} className="flex w-full flex-col gap-1">
|
||||||
className="mb-1 text-left text-sm font-medium text-slate-700/70 dark:text-slate-50/70"
|
<label
|
||||||
>
|
htmlFor={authField}
|
||||||
{config.label}
|
className="mb-1 text-left text-sm font-medium text-slate-700/70 dark:text-slate-50/70"
|
||||||
</label>
|
>
|
||||||
<HoverCard openDelay={300}>
|
{config.label}
|
||||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
</label>
|
||||||
<input
|
<HoverCard openDelay={300}>
|
||||||
type="text"
|
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||||
autoComplete="off"
|
<input
|
||||||
id={config.authField}
|
type="text"
|
||||||
aria-invalid={!!errors[config.authField]}
|
autoComplete="off"
|
||||||
aria-describedby={`${config.authField}-error`}
|
id={authField}
|
||||||
aria-label={config.label}
|
aria-invalid={!!errors[authField]}
|
||||||
aria-required="true"
|
aria-describedby={`${authField}-error`}
|
||||||
{...register(config.authField, {
|
aria-label={config.label}
|
||||||
required: `${config.label} is required.`,
|
aria-required="true"
|
||||||
minLength: {
|
{...register(authField, {
|
||||||
value: 10,
|
required: `${config.label} is required.`,
|
||||||
message: `${config.label} must be at least 10 characters long`,
|
minLength: {
|
||||||
},
|
value: 10,
|
||||||
})}
|
message: `${config.label} must be at least 10 characters long`,
|
||||||
className="flex h-10 max-h-10 w-full resize-none rounded-md border border-gray-200 bg-transparent px-3 py-2 text-sm text-gray-700 shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:border-slate-400 focus:bg-gray-50 focus:outline-none focus:ring-0 focus:ring-gray-400 focus:ring-opacity-0 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 focus:dark:bg-gray-600 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0"
|
},
|
||||||
/>
|
})}
|
||||||
</HoverCardTrigger>
|
className="flex h-10 max-h-10 w-full resize-none rounded-md border border-gray-200 bg-transparent px-3 py-2 text-sm text-gray-700 shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:border-slate-400 focus:bg-gray-50 focus:outline-none focus:ring-0 focus:ring-gray-400 focus:ring-opacity-0 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 focus:dark:bg-gray-600 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0"
|
||||||
<PluginTooltip content={config.description} position="right" />
|
/>
|
||||||
</HoverCard>
|
</HoverCardTrigger>
|
||||||
{errors[config.authField] && (
|
<PluginTooltip content={config.description} position="right" />
|
||||||
<span role="alert" className="mt-1 text-sm text-red-400">
|
</HoverCard>
|
||||||
{/* @ts-ignore - Type 'string | FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined' is not assignable to type 'ReactNode' */}
|
{errors[authField] && (
|
||||||
{errors[config.authField].message}
|
<span role="alert" className="mt-1 text-sm text-red-400">
|
||||||
</span>
|
{/* @ts-ignore - Type 'string | FieldError | Merge<FieldError, FieldErrorsImpl<any>> | undefined' is not assignable to type 'ReactNode' */}
|
||||||
)}
|
{errors[authField].message}
|
||||||
</div>
|
</span>
|
||||||
))}
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<button
|
<button
|
||||||
disabled={!isDirty || !isValid || isSubmitting}
|
disabled={!isDirty || !isValid || isSubmitting}
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
||||||
|
|
@ -407,6 +407,8 @@ AZURE_AI_SEARCH_SEARCH_OPTION_SELECT=
|
||||||
|
|
||||||
#### DALL-E:
|
#### DALL-E:
|
||||||
|
|
||||||
|
**Note:** Make sure the `gptPlugins` endpoint is set in the [`ENDPOINTS`](#endpoints) environment variable if it was configured before.
|
||||||
|
|
||||||
**API Keys:**
|
**API Keys:**
|
||||||
- `DALLE_API_KEY`: This environment variable is intended for storing the OpenAI API key that grants access to both DALL-E 2 and DALL-E 3 services. Typically, this key should be kept private. If you are distributing a plugin or software that integrates with DALL-E, you may choose to leave this commented out, requiring the end user to input their own API key. If you have a shared API key you want to distribute with your software (not recommended for security reasons), you can uncomment this and provide the key.
|
- `DALLE_API_KEY`: This environment variable is intended for storing the OpenAI API key that grants access to both DALL-E 2 and DALL-E 3 services. Typically, this key should be kept private. If you are distributing a plugin or software that integrates with DALL-E, you may choose to leave this commented out, requiring the end user to input their own API key. If you have a shared API key you want to distribute with your software (not recommended for security reasons), you can uncomment this and provide the key.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue