💫 feat: Config File & Custom Endpoints (#1474)

* WIP(backend/api): custom endpoint

* WIP(frontend/client): custom endpoint

* chore: adjust typedefs for configs

* refactor: use data-provider for cache keys and rename enums and custom endpoint for better clarity and compatibility

* feat: loadYaml utility

* refactor: rename back to  from  and proof-of-concept for creating schemas from user-defined defaults

* refactor: remove custom endpoint from default endpointsConfig as it will be exclusively managed by yaml config

* refactor(EndpointController): rename variables for clarity

* feat: initial load custom config

* feat(server/utils): add simple `isUserProvided` helper

* chore(types): update TConfig type

* refactor: remove custom endpoint handling from model services as will be handled by config, modularize fetching of models

* feat: loadCustomConfig, loadConfigEndpoints, loadConfigModels

* chore: reorganize server init imports, invoke loadCustomConfig

* refactor(loadConfigEndpoints/Models): return each custom endpoint as standalone endpoint

* refactor(Endpoint/ModelController): spread config values after default (temporary)

* chore(client): fix type issues

* WIP: first pass for multiple custom endpoints
- add endpointType to Conversation schema
- add update zod schemas for both convo/presets to allow non-EModelEndpoint value as endpoint (also using type assertion)
- use `endpointType` value as `endpoint` where mapping to type is necessary using this field
- use custom defined `endpoint` value and not type for mapping to modelsConfig
- misc: add return type to `getDefaultEndpoint`
- in `useNewConvo`, add the endpointType if it wasn't already added to conversation
- EndpointsMenu: use user-defined endpoint name as Title in menu
- TODO: custom icon via custom config, change unknown to robot icon

* refactor(parseConvo): pass args as an object and change where used accordingly; chore: comment out 'create schema' code

* chore: remove unused availableModels field in TConfig type

* refactor(parseCompactConvo): pass args as an object and change where used accordingly

* feat: chat through custom endpoint

* chore(message/convoSchemas): avoid saving empty arrays

* fix(BaseClient/saveMessageToDatabase): save endpointType

* refactor(ChatRoute): show Spinner if endpointsQuery or modelsQuery are still loading, which is apparent with slow fetching of models/remote config on first serve

* fix(useConversation): assign endpointType if it's missing

* fix(SaveAsPreset): pass real endpoint and endpointType when saving Preset)

* chore: recorganize types order for TConfig, add `iconURL`

* feat: custom endpoint icon support:
- use UnknownIcon in all icon contexts
- add mistral and openrouter as known endpoints, and add their icons
- iconURL support

* fix(presetSchema): move endpointType to default schema definitions shared between convoSchema and defaults

* refactor(Settings/OpenAI): remove legacy `isOpenAI` flag

* fix(OpenAIClient): do not invoke abortCompletion on completion error

* feat: add responseSender/label support for custom endpoints:
- use defaultModelLabel field in endpointOption
- add model defaults for custom endpoints in `getResponseSender`
- add `useGetSender` hook which uses EndpointsQuery to determine `defaultModelLabel`
- include defaultModelLabel from endpointConfig in custom endpoint client options
- pass `endpointType` to `getResponseSender`

* feat(OpenAIClient): use custom options from config file

* refactor: rename `defaultModelLabel` to `modelDisplayLabel`

* refactor(data-provider): separate concerns from `schemas` into `parsers`, `config`, and fix imports elsewhere

* feat: `iconURL` and extract environment variables from custom endpoint config values

* feat: custom config validation via zod schema, rename and move to `./projectRoot/librechat.yaml`

* docs: custom config docs and examples

* fix(OpenAIClient/mistral): mistral does not allow singular system message, also add `useChatCompletion` flag to use openai-node for title completions

* fix(custom/initializeClient): extract env var and use `isUserProvided` function

* Update librechat.example.yaml

* feat(InputWithLabel): add className props, and forwardRef

* fix(streamResponse): handle error edge case where either messages or convos query throws an error

* fix(useSSE): handle errorHandler edge cases where error response is and is not properly formatted from API, especially when a conversationId is not yet provided, which ensures stream is properly closed on error

* feat: user_provided keys for custom endpoints

* fix(config/endpointSchema): do not allow default endpoint values in custom endpoint `name`

* feat(loadConfigModels): extract env variables and optimize fetching models

* feat: support custom endpoint iconURL for messages and Nav

* feat(OpenAIClient): add/dropParams support

* docs: update docs with default params, add/dropParams, and notes to use config file instead of `OPENAI_REVERSE_PROXY`

* docs: update docs with additional notes

* feat(maxTokensMap): add mistral models (32k context)

* docs: update openrouter notes

* Update ai_setup.md

* docs(custom_config): add table of contents and fix note about custom name

* docs(custom_config): reorder ToC

* Update custom_config.md

* Add note about `max_tokens` field in custom_config.md
This commit is contained in:
Danny Avila 2024-01-03 09:22:48 -05:00 committed by GitHub
parent 3f98f92d4c
commit 29473a72db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
100 changed files with 2146 additions and 627 deletions

View file

@ -6,8 +6,10 @@ import {
AzureMinimalIcon,
BingAIMinimalIcon,
GoogleMinimalIcon,
CustomMinimalIcon,
LightningIcon,
} from '~/components/svg';
import UnknownIcon from './UnknownIcon';
import { cn } from '~/utils';
export const icons = {
@ -18,6 +20,7 @@ export const icons = {
[EModelEndpoint.chatGPTBrowser]: LightningIcon,
[EModelEndpoint.google]: GoogleMinimalIcon,
[EModelEndpoint.bingAI]: BingAIMinimalIcon,
[EModelEndpoint.custom]: CustomMinimalIcon,
[EModelEndpoint.assistant]: ({ className = '' }) => (
<svg
width="24"
@ -39,5 +42,5 @@ export const icons = {
></path>
</svg>
),
unknown: GPTIcon,
unknown: UnknownIcon,
};

View file

@ -1,6 +1,7 @@
import { useState } from 'react';
import { Settings } from 'lucide-react';
import { EModelEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { FC } from 'react';
import { useLocalize, useUserKey } from '~/hooks';
import { SetKeyDialog } from '~/components/Input/SetKeyDialog';
@ -26,7 +27,8 @@ const MenuItem: FC<MenuItemProps> = ({
userProvidesKey,
...rest
}) => {
const Icon = icons[endpoint] ?? icons.unknown;
const { data: endpointsConfig } = useGetEndpointsQuery();
const [isDialogOpen, setDialogOpen] = useState(false);
const { newConversation } = useChatContext();
const { getExpiry } = useUserKey(endpoint);
@ -44,6 +46,10 @@ const MenuItem: FC<MenuItemProps> = ({
}
};
const endpointType = endpointsConfig?.[endpoint ?? '']?.type;
const iconKey = endpointType ? 'unknown' : endpoint ?? 'unknown';
const Icon = icons[iconKey];
return (
<>
<div
@ -56,7 +62,15 @@ const MenuItem: FC<MenuItemProps> = ({
<div className="flex grow items-center justify-between gap-2">
<div>
<div className="flex items-center gap-2">
{<Icon size={18} className="icon-md shrink-0 dark:text-white" />}
{
<Icon
size={18}
endpoint={endpoint}
context={'menu-item'}
className="icon-md shrink-0 dark:text-white"
iconURL={endpointsConfig?.[endpoint ?? '']?.iconURL}
/>
}
<div>
{title}
<div className="text-token-text-tertiary">{description}</div>
@ -128,7 +142,13 @@ const MenuItem: FC<MenuItemProps> = ({
</div>
</div>
{userProvidesKey && (
<SetKeyDialog open={isDialogOpen} onOpenChange={setDialogOpen} endpoint={endpoint} />
<SetKeyDialog
open={isDialogOpen}
endpoint={endpoint}
endpointType={endpointType}
onOpenChange={setDialogOpen}
userProvideURL={endpointsConfig?.[endpoint ?? '']?.userProvideURL}
/>
)}
</>
);

View file

@ -0,0 +1,36 @@
import { EModelEndpoint, KnownEndpoints } from 'librechat-data-provider';
import { CustomMinimalIcon } from '~/components/svg';
export default function UnknownIcon({
className = '',
endpoint,
iconURL,
context,
}: {
iconURL?: string;
className?: string;
endpoint: EModelEndpoint | string | null;
context?: 'landing' | 'menu-item' | 'nav' | 'message';
}) {
if (!endpoint) {
return <CustomMinimalIcon className={className} />;
}
const currentEndpoint = endpoint.toLowerCase();
if (iconURL) {
return <img className={className} src={iconURL} alt={`${endpoint} Icon`} />;
} else if (currentEndpoint === KnownEndpoints.mistral) {
return (
<img
className={context === 'landing' ? '' : className}
src="/assets/mistral.png"
alt="Mistral AI Icon"
/>
);
} else if (currentEndpoint === KnownEndpoints.openrouter) {
return <img className={className} src="/assets/openrouter.png" alt="OpenRouter Icon" />;
}
return <CustomMinimalIcon className={className} />;
}

View file

@ -21,7 +21,7 @@ const EndpointsMenu: FC = () => {
}
return (
<Root>
<TitleButton primaryText={(alternateName[selected] ?? '') + ' '} />
<TitleButton primaryText={(alternateName[selected] ?? selected ?? '') + ' '} />
<Portal>
<div
style={{

View file

@ -77,7 +77,6 @@ const EditPresetDialog = ({
{''}
</Label>
<PopoverButtons
endpoint={endpoint}
buttonClass="ml-0 w-full dark:bg-gray-700 dark:hover:bg-gray-800 p-2 h-[40px] justify-center mt-0"
iconClass="hidden lg:block w-4"
/>

View file

@ -2,6 +2,7 @@ import { Trash2 } from 'lucide-react';
import { useRecoilValue } from 'recoil';
import { Close } from '@radix-ui/react-popover';
import { Flipper, Flipped } from 'react-flip-toolkit';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { FC } from 'react';
import type { TPreset } from 'librechat-data-provider';
import FileUpload from '~/components/Input/EndpointMenu/FileUpload';
@ -31,6 +32,7 @@ const PresetItems: FC<{
clearAllPresets,
onFileSelected,
}) => {
const { data: endpointsConfig } = useGetEndpointsQuery();
const defaultPreset = useRecoilValue(store.defaultPreset);
const localize = useLocalize();
return (
@ -93,6 +95,10 @@ const PresetItems: FC<{
return null;
}
const iconKey = endpointsConfig?.[preset.endpoint ?? '']?.type
? 'unknown'
: preset.endpoint ?? 'unknown';
return (
<Close asChild key={`preset-${preset.presetId}`}>
<div key={`preset-${preset.presetId}`}>
@ -103,8 +109,11 @@ const PresetItems: FC<{
title={getPresetTitle(preset)}
disableHover={true}
onClick={() => onSelectPreset(preset)}
icon={icons[preset.endpoint ?? 'unknown']({
icon={icons[iconKey]({
context: 'menu-item',
iconURL: endpointsConfig?.[preset.endpoint ?? ''].iconURL,
className: 'icon-md mr-1 dark:text-white',
endpoint: preset.endpoint,
})}
selected={false}
data-testid={`preset-item-${preset}`}

View file

@ -3,6 +3,7 @@ import { BookCopy } from 'lucide-react';
import { Content, Portal, Root, Trigger } from '@radix-ui/react-popover';
import { EditPresetDialog, PresetItems } from './Presets';
import { useLocalize, usePresets } from '~/hooks';
import { useChatContext } from '~/Providers';
import { cn } from '~/utils';
const PresetsMenu: FC = () => {
@ -18,6 +19,7 @@ const PresetsMenu: FC = () => {
submitPreset,
exportPreset,
} = usePresets();
const { preset } = useChatContext();
const presets = presetsQuery.data || [];
return (
@ -64,7 +66,7 @@ const PresetsMenu: FC = () => {
</Content>
</div>
</Portal>
<EditPresetDialog submitPreset={submitPreset} exportPreset={exportPreset} />
{preset && <EditPresetDialog submitPreset={submitPreset} exportPreset={exportPreset} />}
</Root>
);
};