mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-02-12 04:24:24 +01:00
✋ 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:
parent
4121818124
commit
099aa9dead
29 changed files with 690 additions and 93 deletions
|
|
@ -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',
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue