Added functionality to allow users to set custom api keys (#276)

* Added functionality to allow users to set custom api keys

* Added error handling

* Changed token to apiKey

* Changed apiKey to oaiApiKey

* added azure openai ui

* Removed logging

* Changed configure to Use

* Made checked position more rounded

* Made setting api key optional if it is openai

* Modified error handling

* Add support for insufficient_quota errors

* Fixed faulty error detection

* removed logging
This commit is contained in:
Anirudh 2023-05-18 04:51:30 +05:30 committed by GitHub
parent 08f3a77d58
commit 14104b276f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 207 additions and 32 deletions

View file

@ -7,6 +7,7 @@ const askClient = async ({
parentMessageId,
conversationId,
model,
oaiApiKey,
chatGptLabel,
promptPrefix,
temperature,
@ -41,10 +42,10 @@ const askClient = async ({
// debug: true
};
let apiKey = process.env.OPENAI_KEY;
let apiKey = oaiApiKey ? oaiApiKey : process.env.OPENAI_KEY || null;
if (azure) {
apiKey = process.env.AZURE_OPENAI_API_KEY;
apiKey = oaiApiKey ? oaiApiKey : process.env.AZURE_OPENAI_API_KEY || null;
clientOptions.reverseProxyUrl = genAzureEndpoint({
azureOpenAIApiInstanceName: process.env.AZURE_OPENAI_API_INSTANCE_NAME,
azureOpenAIApiDeploymentName: process.env.AZURE_OPENAI_API_DEPLOYMENT_NAME,

View file

@ -174,6 +174,7 @@ const ask = async ({
text,
parentMessageId: userParentMessageId,
conversationId,
oaiApiKey: req.body?.token ?? null,
...endpointOption,
onProgress: progressCallback.call(null, {
res,

View file

@ -40,7 +40,7 @@ router.get('/', async function (req, res) {
const azureOpenAI = !!process.env.AZURE_OPENAI_KEY;
const openAI =
process.env.OPENAI_KEY || process.env.AZURE_OPENAI_API_KEY
? { availableModels: getOpenAIModels() }
? { availableModels: getOpenAIModels(), userProvide: true }
: false;
const bingAI = process.env.BINGAI_TOKEN
? { userProvide: process.env.BINGAI_TOKEN == 'user_provided' }

View file

@ -31,6 +31,7 @@
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2",
"@radix-ui/react-hover-card": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-slider": "^1.1.1",
"@radix-ui/react-tabs": "^1.0.3",

View file

@ -26,7 +26,7 @@ export default function ModelItem({ endpoint, value, onSelect }) {
className: 'mr-2'
});
const isuserProvide = endpointsConfig?.[endpoint]?.userProvide;
const isUserProvided = endpointsConfig?.[endpoint]?.userProvide;
// regular model
return (
@ -39,7 +39,7 @@ export default function ModelItem({ endpoint, value, onSelect }) {
{alternateName[endpoint] || endpoint}
{!!['azureOpenAI', 'openAI'].find(e => e === endpoint) && <sup>$</sup>}
<div className="flex w-4 flex-1" />
{isuserProvide ? (
{isUserProvided ? (
<button
className="invisible m-0 mr-1 flex-initial rounded-md p-0 text-xs font-medium text-gray-400 hover:text-gray-700 group-hover:visible dark:font-normal dark:text-gray-400 dark:hover:text-gray-200"
onClick={e => {

View file

@ -0,0 +1,34 @@
import React from 'react';
import { Input } from '../../ui/Input.tsx';
import { Label } from '../../ui/Label.tsx';
import { cn } from '~/utils/';
function InputWithLabel({ value, onChange, label, id }) {
const defaultTextProps =
'rounded-md border border-gray-300 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-400 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0';
return (
<>
<Label
htmlFor={id}
className="text-left text-sm font-medium"
>
{label}
<br />
</Label>
<Input
id={id}
value={value || ''}
onChange={onChange}
placeholder={`Enter ${label}`}
className={cn(
defaultTextProps,
'flex h-10 max-h-10 w-full resize-none px-3 py-2 focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0'
)}
/>
</>
);
}
export default InputWithLabel;

View file

@ -4,11 +4,24 @@ import { Dialog } from '../../ui/Dialog.tsx';
import { Input } from '../../ui/Input.tsx';
import { Label } from '../../ui/Label.tsx';
import { cn } from '~/utils/';
import * as Checkbox from '@radix-ui/react-checkbox';
import { CheckIcon } from '@radix-ui/react-icons';
import FileUpload from '../NewConversationMenu/FileUpload';
import store from '~/store';
import InputWithLabel from './InputWithLabel';
function isJson(str) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
const [token, setToken] = useState('');
const [showPanel, setShowPanel] = useState(false);
const { getToken, saveToken } = store.useToken(endpoint);
const defaultTextProps =
@ -20,9 +33,19 @@ const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
};
useEffect(() => {
setToken(getToken() ?? '');
let oldToken = getToken();
if (isJson(token)) {
setShowPanel(true);
}
setToken(oldToken ?? '');
}, [open]);
useEffect(() => {
if (!showPanel && isJson(token)) {
setToken('');
}
}, [showPanel]);
const helpText = {
bingAI: (
<small className="break-all text-gray-600">
@ -79,6 +102,25 @@ const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
)
};
function getAzure(name) {
if (isJson(token)) {
let newToken = JSON.parse(token);
return newToken[name];
} else {
return '';
}
}
function setAzure(name, value) {
let newToken = {};
if (isJson(token)) {
newToken = JSON.parse(token);
}
newToken[name] = value;
setToken(JSON.stringify(newToken));
}
return (
<Dialog
open={open}
@ -88,13 +130,6 @@ const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
title={`Set Token of ${endpoint}`}
main={
<div className="grid w-full items-center gap-2">
<Label
htmlFor="chatGptLabel"
className="text-left text-sm font-medium"
>
Token Name
<br />
</Label>
{endpoint === 'google' ? (
<FileUpload
id="googleKey"
@ -137,17 +172,77 @@ const SetTokenDialog = ({ open, onOpenChange, endpoint }) => {
setToken(JSON.stringify(data));
}}
/>
) : (
<Input
id="chatGptLabel"
) : endpoint === 'openAI' ? (
<>
{!showPanel ? (
<>
<InputWithLabel
id={'chatGPTLabel'}
value={token || ''}
onChange={e => setToken(e.target.value || '')}
placeholder="Set the token."
className={cn(
defaultTextProps,
'flex h-10 max-h-10 w-full resize-none px-3 py-2 focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0'
)}
label={'OpenAI API Key'}
/>
</>
) : (
<>
<InputWithLabel
id={'instanceNameLabel'}
value={getAzure('instanceName') || ''}
onChange={e => setAzure('instanceName', e.target.value || '')}
label={'Azure OpenAI Instance Name'}
/>
<InputWithLabel
id={'deploymentNameLabel'}
value={getAzure('deploymentName') || ''}
onChange={e => setAzure('deploymentName', e.target.value || '')}
label={'Azure OpenAI Deployment Name'}
/>
<InputWithLabel
id={'versionLabel'}
value={getAzure('version') || ''}
onChange={e => setAzure('version', e.target.value || '')}
label={'Azure OpenAI API Version'}
/>
<InputWithLabel
id={'apiKeyLabel'}
value={getAzure('apiKey') || ''}
onChange={e => setAzure('apiKey', e.target.value || '')}
label={'Azure OpenAI API Key'}
/>
</>
)}
<div className="flex items-center">
<Checkbox.Root
className="flex h-[20px] w-[20px] appearance-none items-center justify-center rounded-[4px] bg-gray-100 text-white outline-none hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-900"
id="azureOpenAI"
checked={showPanel}
onCheckedChange={() => setShowPanel(!showPanel)}
>
<Checkbox.Indicator className="flex h-[20px] w-[20px] items-center justify-center rounded-[3.5px] bg-green-600">
<CheckIcon />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="pl-[8px] text-[15px] leading-none dark:text-white"
htmlFor="azureOpenAI"
>
Use Azure OpenAI.
</label>
</div>
</>
) : (
<>
<InputWithLabel
id={'chatGPTLabel'}
value={token || ''}
onChange={e => setToken(e.target.value || '')}
label={'Token Name'}
/>
</>
)}
<small className="text-red-600">Your token will be sent to the server, but not saved.</small>
{helpText?.[endpoint]}

View file

@ -33,7 +33,7 @@ export default function SubmitButton({
type="button"
className="group absolute bottom-0 right-0 flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
>
<div className="m-1 mr-0 rounded-md p-2 pt-[10px] pb-[10px] group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
<div className="m-1 mr-0 rounded-md p-2 pb-[10px] pt-[10px] group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
<StopGeneratingIcon />
</div>
</button>
@ -61,7 +61,7 @@ export default function SubmitButton({
// </div>
// </button>
// );
else if (!isTokenProvided) {
else if (!isTokenProvided && endpoint !== 'openAI') {
return (
<>
<button
@ -69,7 +69,7 @@ export default function SubmitButton({
type="button"
className="group absolute bottom-0 right-0 flex h-[100%] w-auto items-center justify-center bg-transparent p-1 text-gray-500"
>
<div className="m-1 mr-0 rounded-md p-2 pt-[10px] pb-[10px] align-middle text-xs group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
<div className="m-1 mr-0 rounded-md p-2 pb-[10px] pt-[10px] align-middle text-xs group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
<Settings className="mr-1 inline-block w-[18px]" />
Set Token First
</div>
@ -88,7 +88,7 @@ export default function SubmitButton({
disabled={disabled}
className="group absolute bottom-0 right-0 flex h-[100%] w-[50px] items-center justify-center bg-transparent p-1 text-gray-500"
>
<div className="m-1 mr-0 rounded-md p-2 pt-[10px] pb-[10px] group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
<div className="m-1 mr-0 rounded-md p-2 pb-[10px] pt-[10px] group-hover:bg-gray-100 group-disabled:hover:bg-transparent dark:group-hover:bg-gray-900 dark:group-hover:text-gray-400 dark:group-disabled:hover:bg-transparent">
<svg
stroke="currentColor"
fill="none"

View file

@ -12,6 +12,15 @@ import { useGetConversationByIdQuery } from '~/data-provider';
import { cn } from '~/utils/';
import store from '~/store';
function isJson(str) {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
export default function Message({
conversation,
message,
@ -62,6 +71,23 @@ export default function Message({
}
};
const getError = text => {
const match = text.match(/\{[^{}]*\}/);
var json = match ? match[0] : ''
if (isJson(json)) {
json = JSON.parse(json);
if (json.code === 'invalid_api_key') {
return 'Invalid API key. Please check your API key and try again. You can access your API key by clicking on the model logo in the top-left corner of the textbox.';
} else if (json.type === 'insufficient_quota') {
return "We're sorry, but the default API key has reached its limit. To continue using this service, please set up your own API key. You can do this by clicking on the model logo in the top-left corner of the textbox.";
} else {
return `Oops! Something went wrong. Please try again in a few moments. Here's the specific error message we encountered: ${text}`;
}
} else {
return `Oops! Something went wrong. Please try again in a few moments. Here's the specific error message we encountered: ${text}`;
}
};
const props = {
className:
'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 bg-white dark:text-gray-100 group dark:bg-gray-800'
@ -98,7 +124,7 @@ export default function Message({
if (!isSubmitting && !message?.isCreatedByUser) regenerate(message);
};
const copyToClipboard = (setIsCopied) => {
const copyToClipboard = setIsCopied => {
setIsCopied(true);
copy(message?.text);
@ -149,7 +175,7 @@ export default function Message({
{error ? (
<div className="flex flex min-h-[20px] flex-grow flex-col items-start gap-2 gap-4 text-red-500">
<div className="rounded-md border border-red-500 bg-red-500/10 px-3 py-2 text-sm text-gray-600 dark:text-gray-100">
{`An error occurred. Please try again in a few moments.\n\nError message: ${text}`}
{getError(text)}
</div>
</div>
) : edit ? (

View file

@ -37,7 +37,8 @@ const useMessageHandler = () => {
temperature: currentConversation?.temperature ?? 1,
top_p: currentConversation?.top_p ?? 1,
presence_penalty: currentConversation?.presence_penalty ?? 0,
frequency_penalty: currentConversation?.frequency_penalty ?? 0
frequency_penalty: currentConversation?.frequency_penalty ?? 0,
token: endpointsConfig[endpoint]?.userProvide ? getToken() : null
};
responseSender = endpointOption.chatGptLabel ?? 'ChatGPT';
} else if (endpoint === 'google') {
@ -47,7 +48,7 @@ const useMessageHandler = () => {
currentConversation?.model ?? endpointsConfig[endpoint]?.availableModels?.[0] ?? 'chat-bison',
chatGptLabel: currentConversation?.chatGptLabel ?? null,
promptPrefix: currentConversation?.promptPrefix ?? null,
examples: currentConversation?.examples ?? [{ input: { content: '' }, output: { content: '' }}],
examples: currentConversation?.examples ?? [{ input: { content: '' }, output: { content: '' } }],
temperature: currentConversation?.temperature ?? 0.2,
maxOutputTokens: currentConversation?.maxOutputTokens ?? 1024,
topP: currentConversation?.topP ?? 0.95,

16
package-lock.json generated
View file

@ -76,6 +76,7 @@
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2",
"@radix-ui/react-hover-card": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-slider": "^1.1.1",
"@radix-ui/react-tabs": "^1.0.3",
@ -4216,6 +4217,14 @@
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-icons": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz",
"integrity": "sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==",
"peerDependencies": {
"react": "^16.x || ^17.x || ^18.x"
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.0.0",
"license": "MIT",
@ -19346,6 +19355,12 @@
"@radix-ui/react-use-controllable-state": "1.0.0"
}
},
"@radix-ui/react-icons": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz",
"integrity": "sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==",
"requires": {}
},
"@radix-ui/react-id": {
"version": "1.0.0",
"requires": {
@ -20822,6 +20837,7 @@
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2",
"@radix-ui/react-hover-card": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-slider": "^1.1.1",
"@radix-ui/react-tabs": "^1.0.3",