🧑‍💻 refactor: Display Client-facing Errors (#2476)

* fix(Google): allow presets to configure expected maxOutputTokens

* refactor: standardize client-facing errors

* refactor(checkUserKeyExpiry): pass endpoint instead of custom message

* feat(UserService): JSDocs and getUserKeyValues

* refactor: add NO_BASE_URL error type, make use of getUserKeyValues, throw user-specific errors

* ci: update tests with recent changes
This commit is contained in:
Danny Avila 2024-04-21 08:31:54 -04:00 committed by GitHub
parent 6db91978ca
commit c937b8cd07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 343 additions and 149 deletions

View file

@ -1,5 +1,6 @@
const { AnthropicClient } = require('~/app');
const { EModelEndpoint } = require('librechat-data-provider');
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
const { AnthropicClient } = require('~/app');
const initializeClient = async ({ req, res, endpointOption }) => {
const { ANTHROPIC_API_KEY, ANTHROPIC_REVERSE_PROXY, PROXY } = process.env;
@ -7,14 +8,15 @@ const initializeClient = async ({ req, res, endpointOption }) => {
const isUserProvided = ANTHROPIC_API_KEY === 'user_provided';
const anthropicApiKey = isUserProvided
? await getAnthropicUserKey(req.user.id)
? await getUserKey({ userId: req.user.id, name: EModelEndpoint.anthropic })
: ANTHROPIC_API_KEY;
if (!anthropicApiKey) {
throw new Error('Anthropic API key not provided. Please provide it again.');
}
if (expiresAt && isUserProvided) {
checkUserKeyExpiry(
expiresAt,
'Your ANTHROPIC_API_KEY has expired. Please provide your API key again.',
);
checkUserKeyExpiry(expiresAt, EModelEndpoint.anthropic);
}
const client = new AnthropicClient(anthropicApiKey, {
@ -31,8 +33,4 @@ const initializeClient = async ({ req, res, endpointOption }) => {
};
};
const getAnthropicUserKey = async (userId) => {
return await getUserKey({ userId, name: 'anthropic' });
};
module.exports = initializeClient;

View file

@ -1,12 +1,13 @@
const OpenAI = require('openai');
const { HttpsProxyAgent } = require('https-proxy-agent');
const {
ErrorTypes,
EModelEndpoint,
resolveHeaders,
mapModelToAzureConfig,
} = require('librechat-data-provider');
const {
getUserKey,
getUserKeyValues,
getUserKeyExpiry,
checkUserKeyExpiry,
} = require('~/server/services/UserService');
@ -26,18 +27,8 @@ const initializeClient = async ({ req, res, endpointOption, initAppClient = fals
userId: req.user.id,
name: EModelEndpoint.assistants,
});
checkUserKeyExpiry(
expiresAt,
'Your Assistants API key has expired. Please provide your API key again.',
);
userValues = await getUserKey({ userId: req.user.id, name: EModelEndpoint.assistants });
try {
userValues = JSON.parse(userValues);
} catch (e) {
throw new Error(
'Invalid JSON provided for Assistants API user values. Please provide them again.',
);
}
checkUserKeyExpiry(expiresAt, EModelEndpoint.assistants);
userValues = await getUserKeyValues({ userId: req.user.id, name: EModelEndpoint.assistants });
}
let apiKey = userProvidesKey ? userValues.apiKey : ASSISTANTS_API_KEY;
@ -101,6 +92,14 @@ const initializeClient = async ({ req, res, endpointOption, initAppClient = fals
}
}
if (userProvidesKey & !apiKey) {
throw new Error(
JSON.stringify({
type: ErrorTypes.NO_USER_KEY,
}),
);
}
if (!apiKey) {
throw new Error('Assistants API key not provided. Please provide it again.');
}

View file

@ -1,12 +1,14 @@
// const OpenAI = require('openai');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { getUserKey, getUserKeyExpiry } = require('~/server/services/UserService');
const { ErrorTypes } = require('librechat-data-provider');
const { getUserKey, getUserKeyExpiry, getUserKeyValues } = require('~/server/services/UserService');
const initializeClient = require('./initializeClient');
// const { OpenAIClient } = require('~/app');
jest.mock('~/server/services/UserService', () => ({
getUserKey: jest.fn(),
getUserKeyExpiry: jest.fn(),
getUserKeyValues: jest.fn(),
checkUserKeyExpiry: jest.requireActual('~/server/services/UserService').checkUserKeyExpiry,
}));
@ -52,9 +54,7 @@ describe('initializeClient', () => {
process.env.ASSISTANTS_API_KEY = 'user_provided';
process.env.ASSISTANTS_BASE_URL = 'user_provided';
getUserKey.mockResolvedValue(
JSON.stringify({ apiKey: 'user-api-key', baseURL: 'https://user.api.url' }),
);
getUserKeyValues.mockResolvedValue({ apiKey: 'user-api-key', baseURL: 'https://user.api.url' });
getUserKeyExpiry.mockResolvedValue(isoString);
const req = { user: { id: 'user123' }, app };
@ -70,11 +70,24 @@ describe('initializeClient', () => {
process.env.ASSISTANTS_API_KEY = 'user_provided';
getUserKey.mockResolvedValue('invalid-json');
getUserKeyExpiry.mockResolvedValue(isoString);
getUserKeyValues.mockImplementation(() => {
let userValues = getUserKey();
try {
userValues = JSON.parse(userValues);
} catch (e) {
throw new Error(
JSON.stringify({
type: ErrorTypes.INVALID_USER_KEY,
}),
);
}
return userValues;
});
const req = { user: { id: 'user123' } };
const res = {};
await expect(initializeClient({ req, res })).rejects.toThrow(/Invalid JSON/);
await expect(initializeClient({ req, res })).rejects.toThrow(/invalid_user_key/);
});
test('throws error if API key is not provided', async () => {

View file

@ -1,11 +1,12 @@
const {
CacheKeys,
ErrorTypes,
envVarRegex,
EModelEndpoint,
FetchTokenConfig,
extractEnvVariable,
} = require('librechat-data-provider');
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
const getCustomConfig = require('~/server/services/Config/getCustomConfig');
const { fetchModels } = require('~/server/services/ModelService');
const getLogStores = require('~/cache/getLogStores');
@ -48,21 +49,29 @@ const initializeClient = async ({ req, res, endpointOption }) => {
let userValues = null;
if (expiresAt && (userProvidesKey || userProvidesURL)) {
checkUserKeyExpiry(
expiresAt,
`Your API values for ${endpoint} have expired. Please configure them again.`,
);
userValues = await getUserKey({ userId: req.user.id, name: endpoint });
try {
userValues = JSON.parse(userValues);
} catch (e) {
throw new Error(`Invalid JSON provided for ${endpoint} user values.`);
}
checkUserKeyExpiry(expiresAt, endpoint);
userValues = await getUserKeyValues({ userId: req.user.id, name: endpoint });
}
let apiKey = userProvidesKey ? userValues?.apiKey : CUSTOM_API_KEY;
let baseURL = userProvidesURL ? userValues?.baseURL : CUSTOM_BASE_URL;
if (userProvidesKey & !apiKey) {
throw new Error(
JSON.stringify({
type: ErrorTypes.NO_USER_KEY,
}),
);
}
if (userProvidesURL && !baseURL) {
throw new Error(
JSON.stringify({
type: ErrorTypes.NO_BASE_URL,
}),
);
}
if (!apiKey) {
throw new Error(`${endpoint} API key not provided.`);
}

View file

@ -1,6 +1,6 @@
const { GoogleClient } = require('~/app');
const { EModelEndpoint, AuthKeys } = require('librechat-data-provider');
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
const { GoogleClient } = require('~/app');
const initializeClient = async ({ req, res, endpointOption }) => {
const { GOOGLE_KEY, GOOGLE_REVERSE_PROXY, PROXY } = process.env;
@ -9,10 +9,7 @@ const initializeClient = async ({ req, res, endpointOption }) => {
let userKey = null;
if (expiresAt && isUserProvided) {
checkUserKeyExpiry(
expiresAt,
'Your Google Credentials have expired. Please provide your Service Account JSON Key or Generative Language API Key again.',
);
checkUserKeyExpiry(expiresAt, EModelEndpoint.google);
userKey = await getUserKey({ userId: req.user.id, name: EModelEndpoint.google });
}

View file

@ -1,15 +1,10 @@
// file deepcode ignore HardcodedNonCryptoSecret: No hardcoded secrets
const { getUserKey } = require('~/server/services/UserService');
const initializeClient = require('./initializeClient');
const { GoogleClient } = require('~/app');
const { checkUserKeyExpiry, getUserKey } = require('../../UserService');
jest.mock('../../UserService', () => ({
checkUserKeyExpiry: jest.fn().mockImplementation((expiresAt, errorMessage) => {
if (new Date(expiresAt) < new Date()) {
throw new Error(errorMessage);
}
}),
jest.mock('~/server/services/UserService', () => ({
checkUserKeyExpiry: jest.requireActual('~/server/services/UserService').checkUserKeyExpiry,
getUserKey: jest.fn().mockImplementation(() => ({})),
}));
@ -74,13 +69,8 @@ describe('google/initializeClient', () => {
};
const res = {};
const endpointOption = { modelOptions: { model: 'default-model' } };
checkUserKeyExpiry.mockImplementation((expiresAt, errorMessage) => {
throw new Error(errorMessage);
});
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
/Your Google Credentials have expired/,
/expired_user_key/,
);
});
});

View file

@ -3,7 +3,7 @@ const {
mapModelToAzureConfig,
resolveHeaders,
} = require('librechat-data-provider');
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
const { isEnabled, isUserProvided } = require('~/server/utils');
const { getAzureCredentials } = require('~/utils');
const { PluginsClient } = require('~/app');
@ -49,18 +49,8 @@ const initializeClient = async ({ req, res, endpointOption }) => {
let userValues = null;
if (expiresAt && (userProvidesKey || userProvidesURL)) {
checkUserKeyExpiry(
expiresAt,
'Your OpenAI API values have expired. Please provide them again.',
);
userValues = await getUserKey({ userId: req.user.id, name: endpoint });
try {
userValues = JSON.parse(userValues);
} catch (e) {
throw new Error(
`Invalid JSON provided for ${endpoint} user values. Please provide them again.`,
);
}
checkUserKeyExpiry(expiresAt, endpoint);
userValues = await getUserKeyValues({ userId: req.user.id, name: endpoint });
}
let apiKey = userProvidesKey ? userValues?.apiKey : credentials[endpoint];

View file

@ -1,12 +1,13 @@
// gptPlugins/initializeClient.spec.js
const { EModelEndpoint, validateAzureGroups } = require('librechat-data-provider');
const { getUserKey } = require('~/server/services/UserService');
const { EModelEndpoint, ErrorTypes, validateAzureGroups } = require('librechat-data-provider');
const { getUserKey, getUserKeyValues } = require('~/server/services/UserService');
const initializeClient = require('./initializeClient');
const { PluginsClient } = require('~/app');
// Mock getUserKey since it's the only function we want to mock
jest.mock('~/server/services/UserService', () => ({
getUserKey: jest.fn(),
getUserKeyValues: jest.fn(),
checkUserKeyExpiry: jest.requireActual('~/server/services/UserService').checkUserKeyExpiry,
}));
@ -205,7 +206,7 @@ describe('gptPlugins/initializeClient', () => {
const res = {};
const endpointOption = { modelOptions: { model: 'default-model' } };
getUserKey.mockResolvedValue(JSON.stringify({ apiKey: 'test-user-provided-openai-api-key' }));
getUserKeyValues.mockResolvedValue({ apiKey: 'test-user-provided-openai-api-key' });
const { openAIApiKey } = await initializeClient({ req, res, endpointOption });
@ -225,14 +226,12 @@ describe('gptPlugins/initializeClient', () => {
const res = {};
const endpointOption = { modelOptions: { model: 'test-model' } };
getUserKey.mockResolvedValue(
JSON.stringify({
apiKey: JSON.stringify({
azureOpenAIApiKey: 'test-user-provided-azure-api-key',
azureOpenAIApiDeploymentName: 'test-deployment',
}),
getUserKeyValues.mockResolvedValue({
apiKey: JSON.stringify({
azureOpenAIApiKey: 'test-user-provided-azure-api-key',
azureOpenAIApiDeploymentName: 'test-deployment',
}),
);
});
const { azure } = await initializeClient({ req, res, endpointOption });
@ -251,7 +250,9 @@ describe('gptPlugins/initializeClient', () => {
const res = {};
const endpointOption = { modelOptions: { model: 'default-model' } };
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(/Your OpenAI API/);
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
/expired_user_key/,
);
});
test('should throw an error if the user-provided Azure key is invalid JSON', async () => {
@ -268,9 +269,22 @@ describe('gptPlugins/initializeClient', () => {
// Simulate an invalid JSON string returned from getUserKey
getUserKey.mockResolvedValue('invalid-json');
getUserKeyValues.mockImplementation(() => {
let userValues = getUserKey();
try {
userValues = JSON.parse(userValues);
} catch (e) {
throw new Error(
JSON.stringify({
type: ErrorTypes.INVALID_USER_KEY,
}),
);
}
return userValues;
});
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
/Invalid JSON provided/,
/invalid_user_key/,
);
});
@ -305,9 +319,22 @@ describe('gptPlugins/initializeClient', () => {
// Mock getUserKey to return a non-JSON string
getUserKey.mockResolvedValue('not-a-json');
getUserKeyValues.mockImplementation(() => {
let userValues = getUserKey();
try {
userValues = JSON.parse(userValues);
} catch (e) {
throw new Error(
JSON.stringify({
type: ErrorTypes.INVALID_USER_KEY,
}),
);
}
return userValues;
});
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
/Invalid JSON provided for openAI user values/,
/invalid_user_key/,
);
});
@ -369,9 +396,10 @@ describe('gptPlugins/initializeClient', () => {
const res = {};
const endpointOption = {};
getUserKey.mockResolvedValue(
JSON.stringify({ apiKey: 'test', baseURL: 'https://user-provided-url.com' }),
);
getUserKeyValues.mockResolvedValue({
apiKey: 'test',
baseURL: 'https://user-provided-url.com',
});
const result = await initializeClient({ req, res, endpointOption });

View file

@ -1,9 +1,10 @@
const {
ErrorTypes,
EModelEndpoint,
resolveHeaders,
mapModelToAzureConfig,
} = require('librechat-data-provider');
const { getUserKey, checkUserKeyExpiry } = require('~/server/services/UserService');
const { getUserKeyValues, checkUserKeyExpiry } = require('~/server/services/UserService');
const { isEnabled, isUserProvided } = require('~/server/utils');
const { getAzureCredentials } = require('~/utils');
const { OpenAIClient } = require('~/app');
@ -36,18 +37,8 @@ const initializeClient = async ({ req, res, endpointOption }) => {
let userValues = null;
if (expiresAt && (userProvidesKey || userProvidesURL)) {
checkUserKeyExpiry(
expiresAt,
'Your OpenAI API values have expired. Please provide them again.',
);
userValues = await getUserKey({ userId: req.user.id, name: endpoint });
try {
userValues = JSON.parse(userValues);
} catch (e) {
throw new Error(
`Invalid JSON provided for ${endpoint} user values. Please provide them again.`,
);
}
checkUserKeyExpiry(expiresAt, endpoint);
userValues = await getUserKeyValues({ userId: req.user.id, name: endpoint });
}
let apiKey = userProvidesKey ? userValues?.apiKey : credentials[endpoint];
@ -99,8 +90,16 @@ const initializeClient = async ({ req, res, endpointOption }) => {
apiKey = clientOptions.azure.azureOpenAIApiKey;
}
if (userProvidesKey & !apiKey) {
throw new Error(
JSON.stringify({
type: ErrorTypes.NO_USER_KEY,
}),
);
}
if (!apiKey) {
throw new Error(`${endpoint} API key not provided. Please provide it again.`);
throw new Error(`${endpoint} API Key not provided.`);
}
const client = new OpenAIClient(apiKey, clientOptions);

View file

@ -1,11 +1,12 @@
const { EModelEndpoint, validateAzureGroups } = require('librechat-data-provider');
const { getUserKey } = require('~/server/services/UserService');
const { EModelEndpoint, ErrorTypes, validateAzureGroups } = require('librechat-data-provider');
const { getUserKey, getUserKeyValues } = require('~/server/services/UserService');
const initializeClient = require('./initializeClient');
const { OpenAIClient } = require('~/app');
// Mock getUserKey since it's the only function we want to mock
jest.mock('~/server/services/UserService', () => ({
getUserKey: jest.fn(),
getUserKeyValues: jest.fn(),
checkUserKeyExpiry: jest.requireActual('~/server/services/UserService').checkUserKeyExpiry,
}));
@ -200,7 +201,9 @@ describe('initializeClient', () => {
const res = {};
const endpointOption = {};
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(/Your OpenAI API/);
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
/expired_user_key/,
);
});
test('should throw an error if no API keys are provided in the environment', async () => {
@ -217,7 +220,7 @@ describe('initializeClient', () => {
const endpointOption = {};
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
`${EModelEndpoint.openAI} API key not provided.`,
`${EModelEndpoint.openAI} API Key not provided.`,
);
});
@ -241,7 +244,7 @@ describe('initializeClient', () => {
process.env.OPENAI_API_KEY = 'user_provided';
// Mock getUserKey to return the expected key
getUserKey.mockResolvedValue(JSON.stringify({ apiKey: 'test-user-provided-openai-api-key' }));
getUserKeyValues.mockResolvedValue({ apiKey: 'test-user-provided-openai-api-key' });
// Call the initializeClient function
const result = await initializeClient({ req, res, endpointOption });
@ -266,7 +269,9 @@ describe('initializeClient', () => {
// Mock getUserKey to return an invalid key
getUserKey.mockResolvedValue(invalidKey);
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(/Your OpenAI API/);
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
/expired_user_key/,
);
});
test('should throw an error when user-provided values are not valid JSON', async () => {
@ -281,9 +286,22 @@ describe('initializeClient', () => {
// Mock getUserKey to return a non-JSON string
getUserKey.mockResolvedValue('not-a-json');
getUserKeyValues.mockImplementation(() => {
let userValues = getUserKey();
try {
userValues = JSON.parse(userValues);
} catch (e) {
throw new Error(
JSON.stringify({
type: ErrorTypes.INVALID_USER_KEY,
}),
);
}
return userValues;
});
await expect(initializeClient({ req, res, endpointOption })).rejects.toThrow(
/Invalid JSON provided for openAI user values/,
/invalid_user_key/,
);
});
@ -347,9 +365,10 @@ describe('initializeClient', () => {
const res = {};
const endpointOption = {};
getUserKey.mockResolvedValue(
JSON.stringify({ apiKey: 'test', baseURL: 'https://user-provided-url.com' }),
);
getUserKeyValues.mockResolvedValue({
apiKey: 'test',
baseURL: 'https://user-provided-url.com',
});
const result = await initializeClient({ req, res, endpointOption });