🔗 feat: User Provided Base URL for OpenAI endpoints (#1919)

* chore: bump browserslist-db@latest

* refactor(EndpointService): simplify with `generateConfig`, utilize optional baseURL for OpenAI-based endpoints, use `isUserProvided` helper fn wherever needed

* refactor(custom/initializeClient): use standardized naming for common variables

* feat: user provided baseURL for openAI-based endpoints

* refactor(custom/initializeClient): re-order operations

* fix: knownendpoints enum definition and add FetchTokenConfig, bump data-provider

* refactor(custom): use tokenKey dependent on userProvided conditions for caching and fetching endpointTokenConfig, anticipate token rates from custom config

* refactor(custom): assure endpointTokenConfig is only accessed from cache if qualifies for fetching

* fix(ci): update tests for initializeClient based on userProvideURL changes

* fix(EndpointService): correct baseURL env var for assistants: `ASSISTANTS_BASE_URL`

* fix: unnecessary run cancellation on res.close() when response.run is completed

* feat(assistants): user provided URL option

* ci: update tests and add test for `assistants` endpoint

* chore: leaner condition for request closing

* chore: more descriptive error message to provide keys again
This commit is contained in:
Danny Avila 2024-02-28 14:27:19 -05:00 committed by GitHub
parent 53ae2d7bfb
commit 2f92b54787
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 762 additions and 226 deletions

View file

@ -1,80 +1,101 @@
import { useEffect, useState } from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import { useMultipleKeys } from '~/hooks/Input';
import { useFormContext, Controller } from 'react-hook-form';
import InputWithLabel from './InputWithLabel';
import type { TConfigProps } from '~/common';
import { isJson } from '~/utils/json';
const OpenAIConfig = ({ userKey, setUserKey, endpoint }: TConfigProps) => {
const [showPanel, setShowPanel] = useState(endpoint === EModelEndpoint.azureOpenAI);
const { getMultiKey: getAzure, setMultiKey: setAzure } = useMultipleKeys(setUserKey);
useEffect(() => {
if (isJson(userKey)) {
setShowPanel(true);
}
setUserKey('');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!showPanel && isJson(userKey)) {
setUserKey('');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showPanel]);
const OpenAIConfig = ({
endpoint,
userProvideURL,
}: {
endpoint: EModelEndpoint | string;
userProvideURL?: boolean | null;
}) => {
const { control } = useFormContext();
const isAzure = endpoint === EModelEndpoint.azureOpenAI;
return (
<>
{!showPanel ? (
<form className="flex-wrap">
{!isAzure && (
<Controller
name="apiKey"
control={control}
render={({ field }) => (
<InputWithLabel
id="apiKey"
{...field}
label={`${isAzure ? 'Azure q' : ''}OpenAI API Key`}
labelClassName="mb-1"
inputClassName="mb-2"
/>
)}
/>
)}
{isAzure && (
<>
<InputWithLabel
id={endpoint}
value={userKey ?? ''}
onChange={(e: { target: { value: string } }) => setUserKey(e.target.value ?? '')}
label={'OpenAI API Key'}
<Controller
name="azureOpenAIApiKey"
control={control}
render={({ field }) => (
<InputWithLabel
id="azureOpenAIApiKey"
{...field}
label={'Azure OpenAI API Key'}
labelClassName="mb-1"
/>
)}
/>
</>
) : (
<>
<InputWithLabel
id={'instanceNameLabel'}
value={getAzure('azureOpenAIApiInstanceName', userKey) ?? ''}
onChange={(e: { target: { value: string } }) =>
setAzure('azureOpenAIApiInstanceName', e.target.value ?? '', userKey)
}
label={'Azure OpenAI Instance Name'}
<Controller
name="azureOpenAIApiInstanceName"
control={control}
render={({ field }) => (
<InputWithLabel
id="azureOpenAIApiInstanceName"
{...field}
label={'Azure OpenAI Instance Name'}
labelClassName="mb-1"
/>
)}
/>
<InputWithLabel
id={'deploymentNameLabel'}
value={getAzure('azureOpenAIApiDeploymentName', userKey) ?? ''}
onChange={(e: { target: { value: string } }) =>
setAzure('azureOpenAIApiDeploymentName', e.target.value ?? '', userKey)
}
label={'Azure OpenAI Deployment Name'}
<Controller
name="azureOpenAIApiDeploymentName"
control={control}
render={({ field }) => (
<InputWithLabel
id="azureOpenAIApiDeploymentName"
{...field}
label={'Azure OpenAI Deployment Name'}
labelClassName="mb-1"
/>
)}
/>
<InputWithLabel
id={'versionLabel'}
value={getAzure('azureOpenAIApiVersion', userKey) ?? ''}
onChange={(e: { target: { value: string } }) =>
setAzure('azureOpenAIApiVersion', e.target.value ?? '', userKey)
}
label={'Azure OpenAI API Version'}
/>
<InputWithLabel
id={'apiKeyLabel'}
value={getAzure('azureOpenAIApiKey', userKey) ?? ''}
onChange={(e: { target: { value: string } }) =>
setAzure('azureOpenAIApiKey', e.target.value ?? '', userKey)
}
label={'Azure OpenAI API Key'}
<Controller
name="azureOpenAIApiVersion"
control={control}
render={({ field }) => (
<InputWithLabel
id="azureOpenAIApiVersion"
{...field}
label={'Azure OpenAI API Version'}
labelClassName="mb-1"
/>
)}
/>
</>
)}
</>
{userProvideURL && (
<Controller
name="baseURL"
control={control}
render={({ field }) => (
<InputWithLabel
id="baseURL"
{...field}
label={'API Base URL'}
subLabel={'(Optional)'}
labelClassName="mb-1"
/>
)}
/>
)}
</form>
);
};

View file

@ -20,9 +20,18 @@ const endpointComponents = {
[EModelEndpoint.custom]: CustomConfig,
[EModelEndpoint.azureOpenAI]: OpenAIConfig,
[EModelEndpoint.gptPlugins]: OpenAIConfig,
[EModelEndpoint.assistants]: OpenAIConfig,
default: OtherConfig,
};
const formSet: Set<string> = new Set([
EModelEndpoint.openAI,
EModelEndpoint.custom,
EModelEndpoint.azureOpenAI,
EModelEndpoint.gptPlugins,
EModelEndpoint.assistants,
]);
const EXPIRY = {
THIRTY_MINUTES: { display: 'in 30 minutes', value: 30 * 60 * 1000 },
TWO_HOURS: { display: 'in 2 hours', value: 2 * 60 * 60 * 1000 },
@ -47,6 +56,10 @@ const SetKeyDialog = ({
defaultValues: {
apiKey: '',
baseURL: '',
azureOpenAIApiKey: '',
azureOpenAIApiInstanceName: '',
azureOpenAIApiDeploymentName: '',
azureOpenAIApiVersion: '',
// TODO: allow endpoint definitions from user
// name: '',
// TODO: add custom endpoint models defined by user
@ -76,10 +89,26 @@ const SetKeyDialog = ({
onOpenChange(false);
};
if (endpoint === EModelEndpoint.custom || endpointType === EModelEndpoint.custom) {
if (formSet.has(endpoint) || formSet.has(endpointType ?? '')) {
// TODO: handle other user provided options besides baseURL and apiKey
methods.handleSubmit((data) => {
const isAzure = endpoint === EModelEndpoint.azureOpenAI;
const isOpenAIBase =
isAzure ||
endpoint === EModelEndpoint.openAI ||
endpoint === EModelEndpoint.gptPlugins ||
endpoint === EModelEndpoint.assistants;
if (isAzure) {
data.apiKey = 'n/a';
}
const emptyValues = Object.keys(data).filter((key) => {
if (!isAzure && key.startsWith('azure')) {
return false;
}
if (isOpenAIBase && key === 'baseURL') {
return false;
}
if (key === 'baseURL' && !userProvideURL) {
return false;
}
@ -92,10 +121,22 @@ const SetKeyDialog = ({
status: 'error',
});
onOpenChange(true);
} else {
saveKey(JSON.stringify(data));
methods.reset();
return;
}
const { apiKey, baseURL, ...azureOptions } = data;
const userProvidedData = { apiKey, baseURL };
if (isAzure) {
userProvidedData.apiKey = JSON.stringify({
azureOpenAIApiKey: azureOptions.azureOpenAIApiKey,
azureOpenAIApiInstanceName: azureOptions.azureOpenAIApiInstanceName,
azureOpenAIApiDeploymentName: azureOptions.azureOpenAIApiDeploymentName,
azureOpenAIApiVersion: azureOptions.azureOpenAIApiVersion,
});
}
saveKey(JSON.stringify(userProvidedData));
methods.reset();
})();
return;
}