feat: Stop Sequences for Conversations & Presets (#2536)

* feat: `stop` conversation parameter

* feat: Tag primitive

* feat: dynamic tags

* refactor: update tag styling

* feat: add stop sequences to OpenAI settings

* fix(Presentation): prevent `SidePanel` re-renders that flicker side panel

* refactor: use stop placeholder

* feat: type and schema update for `stop` and `TPreset` in generation param related types

* refactor: pass conversation to dynamic settings

* refactor(OpenAIClient): remove default handling for `modelOptions.stop`

* docs: fix Google AI Setup formatting

* feat: current_model

* docs: WIP update

* fix(ChatRoute): prevent default preset override before `hasSetConversation.current` becomes true by including latest conversation state as template

* docs: update docs with more info on `stop`

* chore: bump config_version

* refactor: CURRENT_MODEL handling
This commit is contained in:
Danny Avila 2024-04-25 11:40:17 -04:00 committed by GitHub
parent 4121818124
commit 099aa9dead
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 690 additions and 93 deletions

View file

@ -616,7 +616,7 @@ export enum Constants {
/**
* Key for the Custom Config's version (librechat.yaml).
*/
CONFIG_VERSION = '1.0.6',
CONFIG_VERSION = '1.0.7',
/**
* Standard value for the first message's `parentMessageId` value, to indicate no parent exists.
*/
@ -625,6 +625,10 @@ export enum Constants {
* Fixed, encoded domain length for Azure OpenAI Assistants Function name parsing.
*/
ENCODED_DOMAIN_LENGTH = 10,
/**
* Identifier for using current_model in multi-model requests.
*/
CURRENT_MODEL = 'current_model',
}
/**

View file

@ -1,12 +1,19 @@
import { z, ZodError, ZodIssueCode } from 'zod';
import { z, ZodArray, ZodError, ZodIssueCode } from 'zod';
import { tConversationSchema, googleSettings as google, openAISettings as openAI } from './schemas';
import type { ZodIssue } from 'zod';
import type { TConversation, TSetOption } from './schemas';
import type { TConversation, TSetOption, TPreset } from './schemas';
export type GoogleSettings = Partial<typeof google>;
export type OpenAISettings = Partial<typeof google>;
export type ComponentType = 'input' | 'textarea' | 'slider' | 'checkbox' | 'switch' | 'dropdown';
export type ComponentType =
| 'input'
| 'textarea'
| 'slider'
| 'checkbox'
| 'switch'
| 'dropdown'
| 'tags';
export type OptionType = 'conversation' | 'model' | 'custom';
@ -17,6 +24,15 @@ export enum ComponentTypes {
Checkbox = 'checkbox',
Switch = 'switch',
Dropdown = 'dropdown',
Tags = 'tags',
}
export enum SettingTypes {
Number = 'number',
Boolean = 'boolean',
String = 'string',
Enum = 'enum',
Array = 'array',
}
export enum OptionTypes {
@ -27,8 +43,8 @@ export enum OptionTypes {
export interface SettingDefinition {
key: string;
description?: string;
type: 'number' | 'boolean' | 'string' | 'enum';
default?: number | boolean | string;
type: 'number' | 'boolean' | 'string' | 'enum' | 'array';
default?: number | boolean | string | string[];
showDefault?: boolean;
options?: string[];
range?: SettingRange;
@ -44,14 +60,18 @@ export interface SettingDefinition {
descriptionCode?: boolean;
minText?: number;
maxText?: number;
minTags?: number; // Specific to tags component
maxTags?: number; // Specific to tags component
includeInput?: boolean; // Specific to slider component
descriptionSide?: 'top' | 'right' | 'bottom' | 'left';
}
export type DynamicSettingProps = Partial<SettingDefinition> & {
readonly?: boolean;
settingKey: string;
setOption: TSetOption;
defaultValue?: number | boolean | string;
conversation: TConversation | TPreset | null;
defaultValue?: number | boolean | string | string[];
};
const requiredSettingFields = ['key', 'type', 'component'];
@ -68,9 +88,19 @@ export function generateDynamicSchema(settings: SettingsConfiguration) {
const schemaFields: { [key: string]: z.ZodTypeAny } = {};
for (const setting of settings) {
const { key, type, default: defaultValue, range, options, minText, maxText } = setting;
const {
key,
type,
default: defaultValue,
range,
options,
minText,
maxText,
minTags,
maxTags,
} = setting;
if (type === 'number') {
if (type === SettingTypes.Number) {
let schema = z.number();
if (range) {
schema = schema.min(range.min);
@ -84,7 +114,7 @@ export function generateDynamicSchema(settings: SettingsConfiguration) {
continue;
}
if (type === 'boolean') {
if (type === SettingTypes.Boolean) {
const schema = z.boolean();
if (typeof defaultValue === 'boolean') {
schemaFields[key] = schema.default(defaultValue);
@ -94,7 +124,7 @@ export function generateDynamicSchema(settings: SettingsConfiguration) {
continue;
}
if (type === 'string') {
if (type === SettingTypes.String) {
let schema = z.string();
if (minText) {
schema = schema.min(minText);
@ -110,7 +140,7 @@ export function generateDynamicSchema(settings: SettingsConfiguration) {
continue;
}
if (type === 'enum') {
if (type === SettingTypes.Enum) {
if (!options || options.length === 0) {
console.warn(`Missing or empty 'options' for enum setting '${key}'.`);
continue;
@ -125,6 +155,23 @@ export function generateDynamicSchema(settings: SettingsConfiguration) {
continue;
}
if (type === SettingTypes.Array) {
let schema: z.ZodSchema = z.array(z.string().or(z.number()));
if (minTags && schema instanceof ZodArray) {
schema = schema.min(minTags);
}
if (maxTags && schema instanceof ZodArray) {
schema = schema.max(maxTags);
}
if (defaultValue && Array.isArray(defaultValue)) {
schema = schema.default(defaultValue);
}
schemaFields[key] = schema;
continue;
}
console.warn(`Unsupported setting type: ${type}`);
}
@ -178,17 +225,75 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
}
// check accepted types
if (!['number', 'boolean', 'string', 'enum'].includes(setting.type)) {
const settingTypes = Object.values(SettingTypes);
if (!settingTypes.includes(setting.type as SettingTypes)) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid type for setting ${setting.key}. Must be one of 'number', 'boolean', 'string', 'enum'.`,
message: `Invalid type for setting ${setting.key}. Must be one of ${settingTypes.join(
', ',
)}.`,
path: ['type'],
});
}
// Predefined constraints based on components
if (setting.component === 'input' || setting.component === 'textarea') {
if (setting.type === 'number' && setting.component === 'textarea') {
if (
(setting.component === ComponentTypes.Tags && setting.type !== SettingTypes.Array) ||
(setting.component !== ComponentTypes.Tags && setting.type === SettingTypes.Array)
) {
errors.push({
code: ZodIssueCode.custom,
message: `Tags component for setting ${setting.key} must have type array.`,
path: ['type'],
});
}
if (setting.component === ComponentTypes.Tags) {
if (setting.minTags !== undefined && setting.minTags < 0) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid minTags value for setting ${setting.key}. Must be non-negative.`,
path: ['minTags'],
});
}
if (setting.maxTags !== undefined && setting.maxTags < 0) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid maxTags value for setting ${setting.key}. Must be non-negative.`,
path: ['maxTags'],
});
}
if (setting.default && !Array.isArray(setting.default)) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must be an array.`,
path: ['default'],
});
}
if (setting.default && setting.maxTags && (setting.default as []).length > setting.maxTags) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must have at most ${setting.maxTags} tags.`,
path: ['default'],
});
}
if (setting.default && setting.minTags && (setting.default as []).length < setting.minTags) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must have at least ${setting.minTags} tags.`,
path: ['default'],
});
}
if (!setting.default) {
setting.default = [];
}
}
if (
setting.component === ComponentTypes.Input ||
setting.component === ComponentTypes.Textarea
) {
if (setting.type === SettingTypes.Number && setting.component === ComponentTypes.Textarea) {
errors.push({
code: ZodIssueCode.custom,
message: `Textarea component for setting ${setting.key} must have type string.`,
@ -214,8 +319,8 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
} // Default placeholder
}
if (setting.component === 'slider') {
if (setting.type === 'number' && !setting.range) {
if (setting.component === ComponentTypes.Slider) {
if (setting.type === SettingTypes.Number && !setting.range) {
errors.push({
code: ZodIssueCode.custom,
message: `Slider component for setting ${setting.key} must have a range if type is number.`,
@ -224,7 +329,7 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
// continue;
}
if (
setting.type === 'enum' &&
setting.type === SettingTypes.Enum &&
(!setting.options || setting.options.length < minSliderOptions)
) {
errors.push({
@ -234,17 +339,21 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
});
// continue;
}
setting.includeInput = setting.type === 'number' ? setting.includeInput ?? true : false; // Default to true if type is number
setting.includeInput =
setting.type === SettingTypes.Number ? setting.includeInput ?? true : false; // Default to true if type is number
}
if (setting.component === 'slider' && setting.type === 'number') {
if (setting.component === ComponentTypes.Slider && setting.type === SettingTypes.Number) {
if (setting.default === undefined && setting.range) {
// Set default to the middle of the range if unspecified
setting.default = Math.round((setting.range.min + setting.range.max) / 2);
}
}
if (setting.component === 'checkbox' || setting.component === 'switch') {
if (
setting.component === ComponentTypes.Checkbox ||
setting.component === ComponentTypes.Switch
) {
if (setting.options && setting.options.length > 2) {
errors.push({
code: ZodIssueCode.custom,
@ -253,12 +362,12 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
});
// continue;
}
if (!setting.default && setting.type === 'boolean') {
if (!setting.default && setting.type === SettingTypes.Boolean) {
setting.default = false; // Default to false if type is boolean
}
}
if (setting.component === 'dropdown') {
if (setting.component === ComponentTypes.Dropdown) {
if (!setting.options || setting.options.length < minDropdownOptions) {
errors.push({
code: ZodIssueCode.custom,
@ -283,7 +392,10 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
}
// Validate minText and maxText for input/textarea
if (setting.component === 'input' || setting.component === 'textarea') {
if (
setting.component === ComponentTypes.Input ||
setting.component === ComponentTypes.Textarea
) {
if (setting.minText !== undefined && setting.minText < 0) {
errors.push({
code: ZodIssueCode.custom,
@ -323,7 +435,7 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
}
/* Default value checks */
if (setting.type === 'number' && isNaN(setting.default as number)) {
if (setting.type === SettingTypes.Number && isNaN(setting.default as number)) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must be a number.`,
@ -331,7 +443,7 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
});
}
if (setting.type === 'boolean' && typeof setting.default !== 'boolean') {
if (setting.type === SettingTypes.Boolean && typeof setting.default !== 'boolean') {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must be a boolean.`,
@ -340,7 +452,7 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
}
if (
(setting.type === 'string' || setting.type === 'enum') &&
(setting.type === SettingTypes.String || setting.type === SettingTypes.Enum) &&
typeof setting.default !== 'string'
) {
errors.push({
@ -351,7 +463,7 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
}
if (
setting.type === 'enum' &&
setting.type === SettingTypes.Enum &&
setting.options &&
!setting.options.includes(setting.default as string)
) {
@ -365,7 +477,7 @@ export function validateSettingDefinitions(settings: SettingsConfiguration): voi
}
if (
setting.type === 'number' &&
setting.type === SettingTypes.Number &&
setting.range &&
typeof setting.default === 'number' &&
(setting.default < setting.range.min || setting.default > setting.range.max)

View file

@ -283,6 +283,7 @@ export const tConversationSchema = z.object({
instructions: z.string().optional(),
/** Used to overwrite active conversation settings when saving a Preset */
presetOverride: z.record(z.unknown()).optional(),
stop: z.array(z.string()).optional(),
});
export const tPresetSchema = tConversationSchema
@ -319,7 +320,7 @@ export type TPreset = z.infer<typeof tPresetSchema>;
export type TSetOption = (
param: number | string,
) => (newValue: number | string | boolean | Partial<TPreset>) => void;
) => (newValue: number | string | boolean | string[] | Partial<TPreset>) => void;
export type TConversation = z.infer<typeof tConversationSchema> & {
presetOverride?: Partial<TPreset>;
@ -336,6 +337,7 @@ export const openAISchema = tConversationSchema
frequency_penalty: true,
resendFiles: true,
imageDetail: true,
stop: true,
})
.transform((obj) => ({
...obj,
@ -349,6 +351,7 @@ export const openAISchema = tConversationSchema
resendFiles:
typeof obj.resendFiles === 'boolean' ? obj.resendFiles : openAISettings.resendFiles.default,
imageDetail: obj.imageDetail ?? openAISettings.imageDetail.default,
stop: obj.stop ?? undefined,
}))
.catch(() => ({
model: openAISettings.model.default,
@ -360,6 +363,7 @@ export const openAISchema = tConversationSchema
frequency_penalty: openAISettings.frequency_penalty.default,
resendFiles: openAISettings.resendFiles.default,
imageDetail: openAISettings.imageDetail.default,
stop: undefined,
}));
export const googleSchema = tConversationSchema
@ -568,6 +572,7 @@ export const compactOpenAISchema = tConversationSchema
frequency_penalty: true,
resendFiles: true,
imageDetail: true,
stop: true,
})
.transform((obj: Partial<TConversation>) => {
const newObj: Partial<TConversation> = { ...obj };