diff --git a/api/app/clients/OpenAIClient.js b/api/app/clients/OpenAIClient.js index f66afda4ab..11e9124953 100644 --- a/api/app/clients/OpenAIClient.js +++ b/api/app/clients/OpenAIClient.js @@ -1,6 +1,7 @@ const OpenAI = require('openai'); const { HttpsProxyAgent } = require('https-proxy-agent'); const { + Constants, ImageDetail, EModelEndpoint, resolveHeaders, @@ -20,9 +21,9 @@ const { const { truncateText, formatMessage, - createContextHandlers, CUT_OFF_PROMPT, titleInstruction, + createContextHandlers, } = require('./prompts'); const { encodeAndFormat } = require('~/server/services/Files/images/encode'); const { handleOpenAIErrors } = require('./tools/util'); @@ -200,16 +201,6 @@ class OpenAIClient extends BaseClient { this.setupTokens(); - if (!this.modelOptions.stop && !this.isVisionModel) { - const stopTokens = [this.startToken]; - if (this.endToken && this.endToken !== this.startToken) { - stopTokens.push(this.endToken); - } - stopTokens.push(`\n${this.userLabel}:`); - stopTokens.push('<|diff_marker|>'); - this.modelOptions.stop = stopTokens; - } - if (reverseProxy) { this.completionsUrl = reverseProxy; this.langchainProxy = extractBaseURL(reverseProxy); @@ -729,7 +720,10 @@ class OpenAIClient extends BaseClient { const { OPENAI_TITLE_MODEL } = process.env ?? {}; - const model = this.options.titleModel ?? OPENAI_TITLE_MODEL ?? 'gpt-3.5-turbo'; + let model = this.options.titleModel ?? OPENAI_TITLE_MODEL ?? 'gpt-3.5-turbo'; + if (model === Constants.CURRENT_MODEL) { + model = this.modelOptions.model; + } const modelOptions = { // TODO: remove the gpt fallback and make it specific to endpoint @@ -851,7 +845,11 @@ ${convo} // TODO: remove the gpt fallback and make it specific to endpoint const { OPENAI_SUMMARY_MODEL = 'gpt-3.5-turbo' } = process.env ?? {}; - const model = this.options.summaryModel ?? OPENAI_SUMMARY_MODEL; + let model = this.options.summaryModel ?? OPENAI_SUMMARY_MODEL; + if (model === Constants.CURRENT_MODEL) { + model = this.modelOptions.model; + } + const maxContextTokens = getModelMaxTokens( model, diff --git a/api/models/schema/defaults.js b/api/models/schema/defaults.js index b2ea3a12c7..710e0f3a36 100644 --- a/api/models/schema/defaults.js +++ b/api/models/schema/defaults.js @@ -88,6 +88,7 @@ const conversationPreset = { instructions: { type: String, }, + stop: { type: [{ type: String }], default: undefined }, }; const agentOptions = { diff --git a/client/src/components/Chat/Input/HeaderOptions.tsx b/client/src/components/Chat/Input/HeaderOptions.tsx index 6470b84104..b0823b7cc0 100644 --- a/client/src/components/Chat/Input/HeaderOptions.tsx +++ b/client/src/components/Chat/Input/HeaderOptions.tsx @@ -98,6 +98,7 @@ export default function HeaderOptions() { >
{ + const resizableLayout = localStorage.getItem('react-resizable-panels:layout'); + return resizableLayout ? JSON.parse(resizableLayout) : undefined; + }, []); + const defaultCollapsed = useMemo(() => { + const collapsedPanels = localStorage.getItem('react-resizable-panels:collapsed'); + return collapsedPanels ? JSON.parse(collapsedPanels) : undefined; + }, []); + const fullCollapse = useMemo(() => localStorage.getItem('fullPanelCollapse') === 'true', []); const layout = () => (
diff --git a/client/src/components/Endpoints/EndpointSettings.tsx b/client/src/components/Endpoints/EndpointSettings.tsx index 20ee9cffe7..52e0d9e3d6 100644 --- a/client/src/components/Endpoints/EndpointSettings.tsx +++ b/client/src/components/Endpoints/EndpointSettings.tsx @@ -27,9 +27,7 @@ export default function Settings({ if (OptionComponent) { return ( -
+
endpointType ?? endpoint, [endpoint, endpointType]); + const isOpenAI = useMemo( + () => optionEndpoint === EModelEndpoint.openAI || optionEndpoint === EModelEndpoint.azureOpenAI, + [optionEndpoint], + ); + if (!conversation) { return null; } @@ -70,8 +86,6 @@ export default function Settings({ conversation, setOption, models, readonly }: const setResendFiles = setOption('resendFiles'); const setImageDetail = setOption('imageDetail'); - const optionEndpoint = endpointType ?? endpoint; - return (
@@ -120,6 +134,22 @@ export default function Settings({ conversation, setOption, models, readonly }: )} />
+
+ +
@@ -133,9 +163,10 @@ export default function Settings({ conversation, setOption, models, readonly }: (!!(defaultValue as boolean | undefined)); const selectedValue = useMemo(() => { diff --git a/client/src/components/SidePanel/Parameters/DynamicDropdown.tsx b/client/src/components/SidePanel/Parameters/DynamicDropdown.tsx index 97fb957735..4e298fb872 100644 --- a/client/src/components/SidePanel/Parameters/DynamicDropdown.tsx +++ b/client/src/components/SidePanel/Parameters/DynamicDropdown.tsx @@ -22,9 +22,10 @@ function DynamicDropdown({ showDefault = true, labelCode, descriptionCode, + conversation, }: DynamicSettingProps) { const localize = useLocalize(); - const { conversation = { conversationId: null }, preset } = useChatContext(); + const { preset } = useChatContext(); const [inputValue, setInputValue] = useState(null); const selectedValue = useMemo(() => { diff --git a/client/src/components/SidePanel/Parameters/DynamicInput.tsx b/client/src/components/SidePanel/Parameters/DynamicInput.tsx index 5de5df5304..feccbae784 100644 --- a/client/src/components/SidePanel/Parameters/DynamicInput.tsx +++ b/client/src/components/SidePanel/Parameters/DynamicInput.tsx @@ -22,9 +22,10 @@ function DynamicInput({ labelCode, descriptionCode, placeholderCode, + conversation, }: DynamicSettingProps) { const localize = useLocalize(); - const { conversation = { conversationId: null }, preset } = useChatContext(); + const { preset } = useChatContext(); const [setInputValue, inputValue] = useDebouncedInput({ optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined, diff --git a/client/src/components/SidePanel/Parameters/DynamicSlider.tsx b/client/src/components/SidePanel/Parameters/DynamicSlider.tsx index 275aaeffe5..365f07177a 100644 --- a/client/src/components/SidePanel/Parameters/DynamicSlider.tsx +++ b/client/src/components/SidePanel/Parameters/DynamicSlider.tsx @@ -23,9 +23,10 @@ function DynamicSlider({ includeInput = true, labelCode, descriptionCode, + conversation, }: DynamicSettingProps) { const localize = useLocalize(); - const { conversation = { conversationId: null }, preset } = useChatContext(); + const { preset } = useChatContext(); const isEnum = useMemo(() => !range && options && options.length > 0, [options, range]); const [setInputValue, inputValue] = useDebouncedInput({ diff --git a/client/src/components/SidePanel/Parameters/DynamicSwitch.tsx b/client/src/components/SidePanel/Parameters/DynamicSwitch.tsx index 08fb126be6..f069e899d2 100644 --- a/client/src/components/SidePanel/Parameters/DynamicSwitch.tsx +++ b/client/src/components/SidePanel/Parameters/DynamicSwitch.tsx @@ -19,9 +19,10 @@ function DynamicSwitch({ showDefault = true, labelCode, descriptionCode, + conversation, }: DynamicSettingProps) { const localize = useLocalize(); - const { conversation = { conversationId: null }, preset } = useChatContext(); + const { preset } = useChatContext(); const [inputValue, setInputValue] = useState(!!(defaultValue as boolean | undefined)); useParameterEffects({ preset, diff --git a/client/src/components/SidePanel/Parameters/DynamicTags.tsx b/client/src/components/SidePanel/Parameters/DynamicTags.tsx new file mode 100644 index 0000000000..a028a8aa56 --- /dev/null +++ b/client/src/components/SidePanel/Parameters/DynamicTags.tsx @@ -0,0 +1,193 @@ +// client/src/components/SidePanel/Parameters/DynamicTags.tsx +import { useState, useMemo, useCallback, useRef } from 'react'; +import { OptionTypes } from 'librechat-data-provider'; +import type { DynamicSettingProps } from 'librechat-data-provider'; +import { Label, Input, HoverCard, HoverCardTrigger, Tag } from '~/components/ui'; +import { useChatContext, useToastContext } from '~/Providers'; +import { useLocalize, useParameterEffects } from '~/hooks'; +import { cn, defaultTextProps } from '~/utils'; +import OptionHover from './OptionHover'; +import { ESide } from '~/common'; + +function DynamicTags({ + label, + settingKey, + defaultValue = [], + description, + columnSpan, + setOption, + optionType, + placeholder, + readonly = false, + showDefault = true, + labelCode, + descriptionCode, + placeholderCode, + descriptionSide = ESide.Left, + conversation, + minTags, + maxTags, +}: DynamicSettingProps) { + const localize = useLocalize(); + const { preset } = useChatContext(); + const { showToast } = useToastContext(); + const inputRef = useRef(null); + const [tagText, setTagText] = useState(''); + const [tags, setTags] = useState( + (defaultValue as string[] | undefined) ?? [], + ); + + const updateState = useCallback( + (update: string[]) => { + if (optionType === OptionTypes.Custom) { + // TODO: custom logic, add to payload but not to conversation + setTags(update); + return; + } + setOption(settingKey)(update); + }, + [optionType, setOption, settingKey], + ); + + const onTagClick = useCallback(() => { + if (inputRef.current) { + inputRef.current.focus(); + } + }, [inputRef]); + + const currentTags: string[] | undefined = useMemo(() => { + if (optionType === OptionTypes.Custom) { + // TODO: custom logic, add to payload but not to conversation + return tags; + } + + if (!conversation?.[settingKey]) { + return defaultValue ?? []; + } + + return conversation?.[settingKey]; + }, [conversation, defaultValue, optionType, settingKey, tags]); + + const onTagRemove = useCallback( + (indexToRemove: number) => { + if (!currentTags) { + return; + } + + if (minTags && currentTags.length <= minTags) { + showToast({ + message: localize('com_ui_min_tags', minTags + ''), + status: 'warning', + }); + return; + } + const update = currentTags.filter((_, index) => index !== indexToRemove); + updateState(update); + }, + [localize, minTags, currentTags, showToast, updateState], + ); + + const onTagAdd = useCallback(() => { + if (!tagText) { + return; + } + + let update = [...(currentTags ?? []), tagText]; + if (maxTags && update.length > maxTags) { + showToast({ + message: localize('com_ui_max_tags', maxTags + ''), + status: 'warning', + }); + update = update.slice(-maxTags); + } + updateState(update); + setTagText(''); + }, [tagText, currentTags, updateState, maxTags, showToast, localize]); + + useParameterEffects({ + preset, + settingKey, + defaultValue: typeof defaultValue === 'undefined' ? [] : defaultValue, + inputValue: tags, + setInputValue: setTags, + preventDelayedUpdate: true, + conversation, + }); + + return ( +
+ + +
+ +
+
+
+ {currentTags?.map((tag: string, index: number) => ( + { + onTagRemove(index); + if (inputRef.current) { + inputRef.current.focus(); + } + }} + /> + ))} + { + if (!currentTags) { + return; + } + if (e.key === 'Backspace' && !tagText) { + onTagRemove(currentTags.length - 1); + } + if (e.key === 'Enter') { + onTagAdd(); + } + }} + onChange={(e) => setTagText(e.target.value)} + placeholder={ + placeholderCode ? localize(placeholder ?? '') || placeholder : placeholder + } + className={cn(defaultTextProps, 'flex h-10 max-h-10 px-3 py-2')} + /> +
+
+
+ {description && ( + + )} +
+
+ ); +} + +export default DynamicTags; diff --git a/client/src/components/SidePanel/Parameters/DynamicTextarea.tsx b/client/src/components/SidePanel/Parameters/DynamicTextarea.tsx index f2e1b70b80..f6411b155b 100644 --- a/client/src/components/SidePanel/Parameters/DynamicTextarea.tsx +++ b/client/src/components/SidePanel/Parameters/DynamicTextarea.tsx @@ -22,9 +22,10 @@ function DynamicTextarea({ labelCode, descriptionCode, placeholderCode, + conversation, }: DynamicSettingProps) { const localize = useLocalize(); - const { conversation = { conversationId: null }, preset } = useChatContext(); + const { preset } = useChatContext(); const [setInputValue, inputValue] = useDebouncedInput({ optionKey: optionType !== OptionTypes.Custom ? settingKey : undefined, diff --git a/client/src/components/SidePanel/Parameters/Panel.tsx b/client/src/components/SidePanel/Parameters/Panel.tsx index bb22f1fea3..f277da1692 100644 --- a/client/src/components/SidePanel/Parameters/Panel.tsx +++ b/client/src/components/SidePanel/Parameters/Panel.tsx @@ -5,12 +5,16 @@ import type { SettingsConfiguration, } from 'librechat-data-provider'; import { useSetIndexOptions } from '~/hooks'; -import DynamicDropdown from './DynamicDropdown'; -import DynamicCheckbox from './DynamicCheckbox'; -import DynamicTextarea from './DynamicTextarea'; -import DynamicSlider from './DynamicSlider'; -import DynamicSwitch from './DynamicSwitch'; -import DynamicInput from './DynamicInput'; +import { useChatContext } from '~/Providers'; +import { + DynamicDropdown, + DynamicCheckbox, + DynamicTextarea, + DynamicSlider, + DynamicSwitch, + DynamicInput, + DynamicTags, +} from './'; const settingsConfiguration: SettingsConfiguration = [ { @@ -129,6 +133,22 @@ const settingsConfiguration: SettingsConfiguration = [ showDefault: false, columnSpan: 2, }, + { + key: 'stop', + label: 'com_endpoint_stop', + labelCode: true, + description: 'com_endpoint_openai_stop', + descriptionCode: true, + placeholder: 'com_endpoint_stop_placeholder', + placeholderCode: true, + type: 'array', + default: [], + component: 'tags', + optionType: 'conversation', + columnSpan: 4, + minTags: 1, + maxTags: 4, + }, ]; const componentMapping: Record> = { @@ -138,9 +158,11 @@ const componentMapping: Record setting.key === 'stop') as SettingDefinition; + const Tags = componentMapping[stop.component]; + const { key: stopKey, default: stopDefault, ...stopSettings } = stop; + return (
@@ -184,30 +210,42 @@ export default function Parameters() { defaultValue={inputDefault} {...inputSettings} setOption={setOption} + conversation={conversation} />