LibreChat/packages/data-provider/src/generate.ts
Danny Avila f869d772f7
🪐 feat: Initial OpenAI Responses API Support (#8149)
* chore: update @librechat/agents to v2.4.47

* WIP: temporary auto-toggle responses api for o1/o3-pro

* feat: Enable Responses API for OpenAI models

- Updated the OpenAI client initialization to check for the useResponsesApi parameter in model options.
- Added translations for enabling the Responses API in the UI.
- Introduced useResponsesApi parameter in data provider settings and schemas.
- Updated relevant schemas to include useResponsesApi for conversation and preset configurations.

* refactor: Remove useResponsesApi check from OpenAI client initialization and update translation for Responses API

- Removed the check for useResponsesApi in the OpenAI client initialization.
- Updated the translation for enabling the Responses API to clarify its functionality.

* chore: update @librechat/agents dependency to version 2.4.48

* chore: update @librechat/agents dependency to version 2.4.49

* chore: linting

* chore: linting

* feat: Enhance DynamicSlider and validation for enumMappings

- Added support for enumMappings in DynamicSlider to display values correctly based on enum settings.
- Implemented validation for enumMappings in the generate function to ensure all options have corresponding mappings.
- Added tests for handling empty string options and incomplete enumMappings in the generate.spec.ts file.

* feat: Enhance DynamicSlider localization support

- Added localization handling for mapped values in DynamicSlider when using enumMappings.
- Updated the logic to check if the mapped value is a localization key and return the localized string if applicable.
- Adjusted dependencies in useCallback hooks to include localize for proper functionality.

* feat: Add reasoning summary and effort options to OpenAI configuration and UI

* feat: Add enumMappings for ImageDetail options in parameter settings

* style: Improve styling for DynamicSlider component labels and inputs

* chore: Update reasoning effort description and parameter order for OpenAI params

---------

Co-authored-by: Dustin Healy <dustinhealy1@gmail.com>
2025-06-30 18:34:47 -04:00

634 lines
20 KiB
TypeScript

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, TPreset } from './schemas';
export type GoogleSettings = Partial<typeof google>;
export type OpenAISettings = Partial<typeof google>;
export type ComponentType =
| 'input'
| 'textarea'
| 'slider'
| 'checkbox'
| 'switch'
| 'dropdown'
| 'combobox'
| 'tags';
export type OptionType = 'conversation' | 'model' | 'custom';
export type Option = Record<string, unknown> & {
label?: string;
value: string | number | null;
};
export type OptionWithIcon = Option & { icon?: React.ReactNode };
export enum ComponentTypes {
Input = 'input',
Textarea = 'textarea',
Slider = 'slider',
Checkbox = 'checkbox',
Switch = 'switch',
Dropdown = 'dropdown',
Combobox = 'combobox',
Tags = 'tags',
}
export enum SettingTypes {
Number = 'number',
Boolean = 'boolean',
String = 'string',
Enum = 'enum',
Array = 'array',
}
export enum OptionTypes {
Conversation = 'conversation',
Model = 'model',
Custom = 'custom',
}
export interface SettingDefinition {
key: string;
description?: string;
type: 'number' | 'boolean' | 'string' | 'enum' | 'array';
default?: number | boolean | string | string[];
showLabel?: boolean;
showDefault?: boolean;
options?: string[];
range?: SettingRange;
enumMappings?: Record<string, number | boolean | string>;
component: ComponentType;
optionType?: OptionType;
columnSpan?: number;
columns?: number;
label?: string;
placeholder?: string;
labelCode?: boolean;
placeholderCode?: boolean;
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';
items?: OptionWithIcon[]; // Specific to combobox component
searchPlaceholder?: string; // Specific to combobox component
selectPlaceholder?: string; // Specific to combobox component
searchPlaceholderCode?: boolean; // Specific to combobox component
selectPlaceholderCode?: boolean; // Specific to combobox component
}
export type DynamicSettingProps = Partial<SettingDefinition> & {
readonly?: boolean;
settingKey: string;
setOption: TSetOption;
conversation: Partial<TConversation> | Partial<TPreset> | null;
defaultValue?: number | boolean | string | string[];
className?: string;
inputClassName?: string;
};
const requiredSettingFields = ['key', 'type', 'component'];
export interface SettingRange {
min: number;
max: number;
step?: number;
}
export type SettingsConfiguration = SettingDefinition[];
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,
minTags,
maxTags,
} = setting;
if (type === SettingTypes.Number) {
let schema = z.number();
if (range) {
schema = schema.min(range.min);
schema = schema.max(range.max);
}
if (typeof defaultValue === 'number') {
schemaFields[key] = schema.default(defaultValue);
} else {
schemaFields[key] = schema;
}
continue;
}
if (type === SettingTypes.Boolean) {
const schema = z.boolean();
if (typeof defaultValue === 'boolean') {
schemaFields[key] = schema.default(defaultValue);
} else {
schemaFields[key] = schema;
}
continue;
}
if (type === SettingTypes.String) {
let schema = z.string();
if (minText) {
schema = schema.min(minText);
}
if (maxText) {
schema = schema.max(maxText);
}
if (typeof defaultValue === 'string') {
schemaFields[key] = schema.default(defaultValue);
} else {
schemaFields[key] = schema;
}
continue;
}
if (type === SettingTypes.Enum) {
if (!options || options.length === 0) {
console.warn(`Missing or empty 'options' for enum setting '${key}'.`);
continue;
}
const schema = z.enum(options as [string, ...string[]]);
if (typeof defaultValue === 'string') {
schemaFields[key] = schema.default(defaultValue);
} else {
schemaFields[key] = schema;
}
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}`);
}
return z.object(schemaFields);
}
const ZodTypeToSettingType: Record<string, string | undefined> = {
ZodString: 'string',
ZodNumber: 'number',
ZodBoolean: 'boolean',
};
const minColumns = 1;
const maxColumns = 4;
const minSliderOptions = 2;
const minDropdownOptions = 2;
const minComboboxOptions = 2;
/**
* Validates the provided setting using the constraints unique to each component type.
* @throws {ZodError} Throws a ZodError if any validation fails.
*/
export function validateSettingDefinitions(settings: SettingsConfiguration): void {
const errors: ZodIssue[] = [];
// Validate columns
const columnsSet = new Set<number>();
for (const setting of settings) {
if (setting.columns !== undefined) {
if (setting.columns < minColumns || setting.columns > maxColumns) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid columns value for setting ${setting.key}. Must be between ${minColumns} and ${maxColumns}.`,
path: ['columns'],
});
} else {
columnsSet.add(setting.columns);
}
}
}
const columns = columnsSet.size === 1 ? columnsSet.values().next().value : 2;
for (const setting of settings) {
for (const field of requiredSettingFields) {
if (setting[field as keyof SettingDefinition] === undefined) {
errors.push({
code: ZodIssueCode.custom,
message: `Missing required field ${field} for setting ${setting.key}.`,
path: [field],
});
}
}
// check accepted types
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 ${settingTypes.join(
', ',
)}.`,
path: ['type'],
});
}
// Predefined constraints based on components
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.`,
path: ['type'],
});
// continue;
}
if (
setting.minText !== undefined &&
setting.maxText !== undefined &&
setting.minText > setting.maxText
) {
errors.push({
code: ZodIssueCode.custom,
message: `For setting ${setting.key}, minText cannot be greater than maxText.`,
path: [setting.key, 'minText', 'maxText'],
});
// continue;
}
if (!setting.placeholder) {
setting.placeholder = '';
} // Default placeholder
}
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.`,
path: ['range'],
});
// continue;
}
if (
setting.type === SettingTypes.Enum &&
(!setting.options || setting.options.length < minSliderOptions)
) {
errors.push({
code: ZodIssueCode.custom,
message: `Slider component for setting ${setting.key} requires at least ${minSliderOptions} options for enum type.`,
path: ['options'],
});
// continue;
}
setting.includeInput =
setting.type === SettingTypes.Number ? (setting.includeInput ?? true) : false; // Default to true if type is 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 === ComponentTypes.Checkbox ||
setting.component === ComponentTypes.Switch
) {
if (setting.options && setting.options.length > 2) {
errors.push({
code: ZodIssueCode.custom,
message: `Checkbox/Switch component for setting ${setting.key} must have 1-2 options.`,
path: ['options'],
});
// continue;
}
if (!setting.default && setting.type === SettingTypes.Boolean) {
setting.default = false; // Default to false if type is boolean
}
}
if (setting.component === ComponentTypes.Dropdown) {
if (!setting.options || setting.options.length < minDropdownOptions) {
errors.push({
code: ZodIssueCode.custom,
message: `Dropdown component for setting ${setting.key} requires at least ${minDropdownOptions} options.`,
path: ['options'],
});
// continue;
}
if (!setting.default && setting.options && setting.options.length > 0) {
setting.default = setting.options[0]; // Default to first option if not specified
}
}
if (setting.component === ComponentTypes.Combobox) {
if (!setting.options || setting.options.length < minComboboxOptions) {
errors.push({
code: ZodIssueCode.custom,
message: `Combobox component for setting ${setting.key} requires at least ${minComboboxOptions} options.`,
path: ['options'],
});
}
if (!setting.default && setting.options && setting.options.length > 0) {
setting.default = setting.options[0];
}
}
// Default columnSpan
if (!setting.columnSpan) {
setting.columnSpan = Math.floor((columns ?? 0) / 2);
}
// Default label to key
if (!setting.label) {
setting.label = setting.key;
}
// Validate minText and maxText for input/textarea
if (
setting.component === ComponentTypes.Input ||
setting.component === ComponentTypes.Textarea
) {
if (setting.minText !== undefined && setting.minText < 0) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid minText value for setting ${setting.key}. Must be non-negative.`,
path: ['minText'],
});
}
if (setting.maxText !== undefined && setting.maxText < 0) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid maxText value for setting ${setting.key}. Must be non-negative.`,
path: ['maxText'],
});
}
}
// Validate optionType and conversation schema
if (setting.optionType !== OptionTypes.Custom) {
const conversationSchema =
tConversationSchema.shape[setting.key as keyof Omit<TConversation, 'disableParams'>];
if (!conversationSchema) {
errors.push({
code: ZodIssueCode.custom,
message: `Setting ${setting.key} with optionType "${setting.optionType}" must be defined in tConversationSchema.`,
path: ['optionType'],
});
} else {
const zodType = conversationSchema._def.typeName;
const settingTypeEquivalent = ZodTypeToSettingType[zodType] || null;
if (settingTypeEquivalent !== setting.type) {
errors.push({
code: ZodIssueCode.custom,
message: `Setting ${setting.key} with optionType "${setting.optionType}" must match the type defined in tConversationSchema.`,
path: ['optionType'],
});
}
}
}
/* Default value checks */
if (
setting.type === SettingTypes.Number &&
isNaN(setting.default as number) &&
setting.default != null
) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must be a number.`,
path: ['default'],
});
}
if (
setting.type === SettingTypes.Boolean &&
typeof setting.default !== 'boolean' &&
setting.default != null
) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must be a boolean.`,
path: ['default'],
});
}
if (
(setting.type === SettingTypes.String || setting.type === SettingTypes.Enum) &&
typeof setting.default !== 'string' &&
setting.default != null
) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must be a string.`,
path: ['default'],
});
}
if (
setting.type === SettingTypes.Enum &&
setting.options &&
!setting.options.includes(setting.default as string)
) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${
setting.key
}. Must be one of the options: [${setting.options.join(', ')}].`,
path: ['default'],
});
}
if (
setting.type === SettingTypes.Number &&
setting.range &&
typeof setting.default === 'number' &&
(setting.default < setting.range.min || setting.default > setting.range.max)
) {
errors.push({
code: ZodIssueCode.custom,
message: `Invalid default value for setting ${setting.key}. Must be within the range [${setting.range.min}, ${setting.range.max}].`,
path: ['default'],
});
}
// Validate enumMappings
if (setting.enumMappings && setting.type === SettingTypes.Enum && setting.options) {
for (const option of setting.options) {
if (!(option in setting.enumMappings)) {
errors.push({
code: ZodIssueCode.custom,
message: `Missing enumMapping for option "${option}" in setting ${setting.key}.`,
path: ['enumMappings'],
});
}
}
}
}
if (errors.length > 0) {
throw new ZodError(errors);
}
}
export const generateOpenAISchema = (customOpenAI: OpenAISettings) => {
const defaults = { ...openAI, ...customOpenAI };
return tConversationSchema
.pick({
model: true,
chatGptLabel: true,
promptPrefix: true,
temperature: true,
top_p: true,
presence_penalty: true,
frequency_penalty: true,
resendFiles: true,
imageDetail: true,
maxContextTokens: true,
})
.transform((obj) => ({
...obj,
model: obj.model ?? defaults.model.default,
chatGptLabel: obj.chatGptLabel ?? null,
promptPrefix: obj.promptPrefix ?? null,
temperature: obj.temperature ?? defaults.temperature.default,
top_p: obj.top_p ?? defaults.top_p.default,
presence_penalty: obj.presence_penalty ?? defaults.presence_penalty.default,
frequency_penalty: obj.frequency_penalty ?? defaults.frequency_penalty.default,
resendFiles:
typeof obj.resendFiles === 'boolean' ? obj.resendFiles : defaults.resendFiles.default,
imageDetail: obj.imageDetail ?? defaults.imageDetail.default,
maxContextTokens: obj.maxContextTokens ?? undefined,
}))
.catch(() => ({
model: defaults.model.default,
chatGptLabel: null,
promptPrefix: null,
temperature: defaults.temperature.default,
top_p: defaults.top_p.default,
presence_penalty: defaults.presence_penalty.default,
frequency_penalty: defaults.frequency_penalty.default,
resendFiles: defaults.resendFiles.default,
imageDetail: defaults.imageDetail.default,
maxContextTokens: undefined,
}));
};
export const generateGoogleSchema = (customGoogle: GoogleSettings) => {
const defaults = { ...google, ...customGoogle };
return tConversationSchema
.pick({
model: true,
modelLabel: true,
promptPrefix: true,
examples: true,
temperature: true,
maxOutputTokens: true,
topP: true,
topK: true,
maxContextTokens: true,
})
.transform((obj) => {
return {
...obj,
model: obj.model ?? defaults.model.default,
modelLabel: obj.modelLabel ?? null,
promptPrefix: obj.promptPrefix ?? null,
examples: obj.examples ?? [{ input: { content: '' }, output: { content: '' } }],
temperature: obj.temperature ?? defaults.temperature.default,
maxOutputTokens: obj.maxOutputTokens ?? defaults.maxOutputTokens.default,
topP: obj.topP ?? defaults.topP.default,
topK: obj.topK ?? defaults.topK.default,
maxContextTokens: obj.maxContextTokens ?? undefined,
};
})
.catch(() => ({
model: defaults.model.default,
modelLabel: null,
promptPrefix: null,
examples: [{ input: { content: '' }, output: { content: '' } }],
temperature: defaults.temperature.default,
maxOutputTokens: defaults.maxOutputTokens.default,
topP: defaults.topP.default,
topK: defaults.topK.default,
maxContextTokens: undefined,
}));
};