librechat-data-provider 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,474 @@
1
+ import { z, ZodError, ZodIssueCode } from 'zod';
2
+ import { tConversationSchema, googleSettings as google, openAISettings as openAI } from './schemas';
3
+ import type { ZodIssue } from 'zod';
4
+ import type { TConversation, TSetOption } from './schemas';
5
+
6
+ export type GoogleSettings = Partial<typeof google>;
7
+ export type OpenAISettings = Partial<typeof google>;
8
+
9
+ export type ComponentType = 'input' | 'textarea' | 'slider' | 'checkbox' | 'switch' | 'dropdown';
10
+
11
+ export type OptionType = 'conversation' | 'model' | 'custom';
12
+
13
+ export enum ComponentTypes {
14
+ Input = 'input',
15
+ Textarea = 'textarea',
16
+ Slider = 'slider',
17
+ Checkbox = 'checkbox',
18
+ Switch = 'switch',
19
+ Dropdown = 'dropdown',
20
+ }
21
+
22
+ export enum OptionTypes {
23
+ Conversation = 'conversation',
24
+ Model = 'model',
25
+ Custom = 'custom',
26
+ }
27
+ export interface SettingDefinition {
28
+ key: string;
29
+ description?: string;
30
+ type: 'number' | 'boolean' | 'string' | 'enum';
31
+ default?: number | boolean | string;
32
+ showDefault?: boolean;
33
+ options?: string[];
34
+ range?: SettingRange;
35
+ enumMappings?: Record<string, number | boolean | string>;
36
+ component: ComponentType;
37
+ optionType?: OptionType;
38
+ columnSpan?: number;
39
+ columns?: number;
40
+ label?: string;
41
+ placeholder?: string;
42
+ labelCode?: boolean;
43
+ placeholderCode?: boolean;
44
+ descriptionCode?: boolean;
45
+ minText?: number;
46
+ maxText?: number;
47
+ includeInput?: boolean; // Specific to slider component
48
+ }
49
+
50
+ export type DynamicSettingProps = Partial<SettingDefinition> & {
51
+ readonly?: boolean;
52
+ settingKey: string;
53
+ setOption: TSetOption;
54
+ defaultValue?: number | boolean | string;
55
+ };
56
+
57
+ const requiredSettingFields = ['key', 'type', 'component'];
58
+
59
+ export interface SettingRange {
60
+ min: number;
61
+ max: number;
62
+ step?: number;
63
+ }
64
+
65
+ export type SettingsConfiguration = SettingDefinition[];
66
+
67
+ export function generateDynamicSchema(settings: SettingsConfiguration) {
68
+ const schemaFields: { [key: string]: z.ZodTypeAny } = {};
69
+
70
+ for (const setting of settings) {
71
+ const { key, type, default: defaultValue, range, options, minText, maxText } = setting;
72
+
73
+ if (type === 'number') {
74
+ let schema = z.number();
75
+ if (range) {
76
+ schema = schema.min(range.min);
77
+ schema = schema.max(range.max);
78
+ }
79
+ if (typeof defaultValue === 'number') {
80
+ schemaFields[key] = schema.default(defaultValue);
81
+ } else {
82
+ schemaFields[key] = schema;
83
+ }
84
+ continue;
85
+ }
86
+
87
+ if (type === 'boolean') {
88
+ const schema = z.boolean();
89
+ if (typeof defaultValue === 'boolean') {
90
+ schemaFields[key] = schema.default(defaultValue);
91
+ } else {
92
+ schemaFields[key] = schema;
93
+ }
94
+ continue;
95
+ }
96
+
97
+ if (type === 'string') {
98
+ let schema = z.string();
99
+ if (minText) {
100
+ schema = schema.min(minText);
101
+ }
102
+ if (maxText) {
103
+ schema = schema.max(maxText);
104
+ }
105
+ if (typeof defaultValue === 'string') {
106
+ schemaFields[key] = schema.default(defaultValue);
107
+ } else {
108
+ schemaFields[key] = schema;
109
+ }
110
+ continue;
111
+ }
112
+
113
+ if (type === 'enum') {
114
+ if (!options || options.length === 0) {
115
+ console.warn(`Missing or empty 'options' for enum setting '${key}'.`);
116
+ continue;
117
+ }
118
+
119
+ const schema = z.enum(options as [string, ...string[]]);
120
+ if (typeof defaultValue === 'string') {
121
+ schemaFields[key] = schema.default(defaultValue);
122
+ } else {
123
+ schemaFields[key] = schema;
124
+ }
125
+ continue;
126
+ }
127
+
128
+ console.warn(`Unsupported setting type: ${type}`);
129
+ }
130
+
131
+ return z.object(schemaFields);
132
+ }
133
+
134
+ const ZodTypeToSettingType: Record<string, string | undefined> = {
135
+ ZodString: 'string',
136
+ ZodNumber: 'number',
137
+ ZodBoolean: 'boolean',
138
+ };
139
+
140
+ const minColumns = 1;
141
+ const maxColumns = 4;
142
+ const minSliderOptions = 2;
143
+ const minDropdownOptions = 2;
144
+
145
+ /**
146
+ * Validates the provided setting using the constraints unique to each component type.
147
+ * @throws {ZodError} Throws a ZodError if any validation fails.
148
+ */
149
+ export function validateSettingDefinitions(settings: SettingsConfiguration): void {
150
+ const errors: ZodIssue[] = [];
151
+ // Validate columns
152
+ const columnsSet = new Set<number>();
153
+ for (const setting of settings) {
154
+ if (setting.columns !== undefined) {
155
+ if (setting.columns < minColumns || setting.columns > maxColumns) {
156
+ errors.push({
157
+ code: ZodIssueCode.custom,
158
+ message: `Invalid columns value for setting ${setting.key}. Must be between ${minColumns} and ${maxColumns}.`,
159
+ path: ['columns'],
160
+ });
161
+ } else {
162
+ columnsSet.add(setting.columns);
163
+ }
164
+ }
165
+ }
166
+
167
+ const columns = columnsSet.size === 1 ? columnsSet.values().next().value : 2;
168
+
169
+ for (const setting of settings) {
170
+ for (const field of requiredSettingFields) {
171
+ if (setting[field as keyof SettingDefinition] === undefined) {
172
+ errors.push({
173
+ code: ZodIssueCode.custom,
174
+ message: `Missing required field ${field} for setting ${setting.key}.`,
175
+ path: [field],
176
+ });
177
+ }
178
+ }
179
+
180
+ // check accepted types
181
+ if (!['number', 'boolean', 'string', 'enum'].includes(setting.type)) {
182
+ errors.push({
183
+ code: ZodIssueCode.custom,
184
+ message: `Invalid type for setting ${setting.key}. Must be one of 'number', 'boolean', 'string', 'enum'.`,
185
+ path: ['type'],
186
+ });
187
+ }
188
+
189
+ // Predefined constraints based on components
190
+ if (setting.component === 'input' || setting.component === 'textarea') {
191
+ if (setting.type === 'number' && setting.component === 'textarea') {
192
+ errors.push({
193
+ code: ZodIssueCode.custom,
194
+ message: `Textarea component for setting ${setting.key} must have type string.`,
195
+ path: ['type'],
196
+ });
197
+ // continue;
198
+ }
199
+
200
+ if (
201
+ setting.minText !== undefined &&
202
+ setting.maxText !== undefined &&
203
+ setting.minText > setting.maxText
204
+ ) {
205
+ errors.push({
206
+ code: ZodIssueCode.custom,
207
+ message: `For setting ${setting.key}, minText cannot be greater than maxText.`,
208
+ path: [setting.key, 'minText', 'maxText'],
209
+ });
210
+ // continue;
211
+ }
212
+ if (!setting.placeholder) {
213
+ setting.placeholder = '';
214
+ } // Default placeholder
215
+ }
216
+
217
+ if (setting.component === 'slider') {
218
+ if (setting.type === 'number' && !setting.range) {
219
+ errors.push({
220
+ code: ZodIssueCode.custom,
221
+ message: `Slider component for setting ${setting.key} must have a range if type is number.`,
222
+ path: ['range'],
223
+ });
224
+ // continue;
225
+ }
226
+ if (
227
+ setting.type === 'enum' &&
228
+ (!setting.options || setting.options.length < minSliderOptions)
229
+ ) {
230
+ errors.push({
231
+ code: ZodIssueCode.custom,
232
+ message: `Slider component for setting ${setting.key} requires at least ${minSliderOptions} options for enum type.`,
233
+ path: ['options'],
234
+ });
235
+ // continue;
236
+ }
237
+ setting.includeInput = setting.type === 'number' ? setting.includeInput ?? true : false; // Default to true if type is number
238
+ }
239
+
240
+ if (setting.component === 'slider' && setting.type === 'number') {
241
+ if (setting.default === undefined && setting.range) {
242
+ // Set default to the middle of the range if unspecified
243
+ setting.default = Math.round((setting.range.min + setting.range.max) / 2);
244
+ }
245
+ }
246
+
247
+ if (setting.component === 'checkbox' || setting.component === 'switch') {
248
+ if (setting.options && setting.options.length > 2) {
249
+ errors.push({
250
+ code: ZodIssueCode.custom,
251
+ message: `Checkbox/Switch component for setting ${setting.key} must have 1-2 options.`,
252
+ path: ['options'],
253
+ });
254
+ // continue;
255
+ }
256
+ if (!setting.default && setting.type === 'boolean') {
257
+ setting.default = false; // Default to false if type is boolean
258
+ }
259
+ }
260
+
261
+ if (setting.component === 'dropdown') {
262
+ if (!setting.options || setting.options.length < minDropdownOptions) {
263
+ errors.push({
264
+ code: ZodIssueCode.custom,
265
+ message: `Dropdown component for setting ${setting.key} requires at least ${minDropdownOptions} options.`,
266
+ path: ['options'],
267
+ });
268
+ // continue;
269
+ }
270
+ if (!setting.default && setting.options && setting.options.length > 0) {
271
+ setting.default = setting.options[0]; // Default to first option if not specified
272
+ }
273
+ }
274
+
275
+ // Default columnSpan
276
+ if (!setting.columnSpan) {
277
+ setting.columnSpan = Math.floor(columns / 2);
278
+ }
279
+
280
+ // Default label to key
281
+ if (!setting.label) {
282
+ setting.label = setting.key;
283
+ }
284
+
285
+ // Validate minText and maxText for input/textarea
286
+ if (setting.component === 'input' || setting.component === 'textarea') {
287
+ if (setting.minText !== undefined && setting.minText < 0) {
288
+ errors.push({
289
+ code: ZodIssueCode.custom,
290
+ message: `Invalid minText value for setting ${setting.key}. Must be non-negative.`,
291
+ path: ['minText'],
292
+ });
293
+ }
294
+ if (setting.maxText !== undefined && setting.maxText < 0) {
295
+ errors.push({
296
+ code: ZodIssueCode.custom,
297
+ message: `Invalid maxText value for setting ${setting.key}. Must be non-negative.`,
298
+ path: ['maxText'],
299
+ });
300
+ }
301
+ }
302
+
303
+ // Validate optionType and conversation schema
304
+ if (setting.optionType !== OptionTypes.Custom) {
305
+ const conversationSchema = tConversationSchema.shape[setting.key as keyof TConversation];
306
+ if (!conversationSchema) {
307
+ errors.push({
308
+ code: ZodIssueCode.custom,
309
+ message: `Setting ${setting.key} with optionType "${setting.optionType}" must be defined in tConversationSchema.`,
310
+ path: ['optionType'],
311
+ });
312
+ } else {
313
+ const zodType = conversationSchema._def.typeName;
314
+ const settingTypeEquivalent = ZodTypeToSettingType[zodType] || null;
315
+ if (settingTypeEquivalent !== setting.type) {
316
+ errors.push({
317
+ code: ZodIssueCode.custom,
318
+ message: `Setting ${setting.key} with optionType "${setting.optionType}" must match the type defined in tConversationSchema.`,
319
+ path: ['optionType'],
320
+ });
321
+ }
322
+ }
323
+ }
324
+
325
+ /* Default value checks */
326
+ if (setting.type === 'number' && isNaN(setting.default as number)) {
327
+ errors.push({
328
+ code: ZodIssueCode.custom,
329
+ message: `Invalid default value for setting ${setting.key}. Must be a number.`,
330
+ path: ['default'],
331
+ });
332
+ }
333
+
334
+ if (setting.type === 'boolean' && typeof setting.default !== 'boolean') {
335
+ errors.push({
336
+ code: ZodIssueCode.custom,
337
+ message: `Invalid default value for setting ${setting.key}. Must be a boolean.`,
338
+ path: ['default'],
339
+ });
340
+ }
341
+
342
+ if (
343
+ (setting.type === 'string' || setting.type === 'enum') &&
344
+ typeof setting.default !== 'string'
345
+ ) {
346
+ errors.push({
347
+ code: ZodIssueCode.custom,
348
+ message: `Invalid default value for setting ${setting.key}. Must be a string.`,
349
+ path: ['default'],
350
+ });
351
+ }
352
+
353
+ if (
354
+ setting.type === 'enum' &&
355
+ setting.options &&
356
+ !setting.options.includes(setting.default as string)
357
+ ) {
358
+ errors.push({
359
+ code: ZodIssueCode.custom,
360
+ message: `Invalid default value for setting ${
361
+ setting.key
362
+ }. Must be one of the options: [${setting.options.join(', ')}].`,
363
+ path: ['default'],
364
+ });
365
+ }
366
+
367
+ if (
368
+ setting.type === 'number' &&
369
+ setting.range &&
370
+ typeof setting.default === 'number' &&
371
+ (setting.default < setting.range.min || setting.default > setting.range.max)
372
+ ) {
373
+ errors.push({
374
+ code: ZodIssueCode.custom,
375
+ message: `Invalid default value for setting ${setting.key}. Must be within the range [${setting.range.min}, ${setting.range.max}].`,
376
+ path: ['default'],
377
+ });
378
+ }
379
+ }
380
+
381
+ if (errors.length > 0) {
382
+ throw new ZodError(errors);
383
+ }
384
+ }
385
+
386
+ export const generateOpenAISchema = (customOpenAI: OpenAISettings) => {
387
+ const defaults = { ...openAI, ...customOpenAI };
388
+ return tConversationSchema
389
+ .pick({
390
+ model: true,
391
+ chatGptLabel: true,
392
+ promptPrefix: true,
393
+ temperature: true,
394
+ top_p: true,
395
+ presence_penalty: true,
396
+ frequency_penalty: true,
397
+ resendFiles: true,
398
+ imageDetail: true,
399
+ })
400
+ .transform((obj) => ({
401
+ ...obj,
402
+ model: obj.model ?? defaults.model.default,
403
+ chatGptLabel: obj.chatGptLabel ?? null,
404
+ promptPrefix: obj.promptPrefix ?? null,
405
+ temperature: obj.temperature ?? defaults.temperature.default,
406
+ top_p: obj.top_p ?? defaults.top_p.default,
407
+ presence_penalty: obj.presence_penalty ?? defaults.presence_penalty.default,
408
+ frequency_penalty: obj.frequency_penalty ?? defaults.frequency_penalty.default,
409
+ resendFiles:
410
+ typeof obj.resendFiles === 'boolean' ? obj.resendFiles : defaults.resendFiles.default,
411
+ imageDetail: obj.imageDetail ?? defaults.imageDetail.default,
412
+ }))
413
+ .catch(() => ({
414
+ model: defaults.model.default,
415
+ chatGptLabel: null,
416
+ promptPrefix: null,
417
+ temperature: defaults.temperature.default,
418
+ top_p: defaults.top_p.default,
419
+ presence_penalty: defaults.presence_penalty.default,
420
+ frequency_penalty: defaults.frequency_penalty.default,
421
+ resendFiles: defaults.resendFiles.default,
422
+ imageDetail: defaults.imageDetail.default,
423
+ }));
424
+ };
425
+
426
+ export const generateGoogleSchema = (customGoogle: GoogleSettings) => {
427
+ const defaults = { ...google, ...customGoogle };
428
+ return tConversationSchema
429
+ .pick({
430
+ model: true,
431
+ modelLabel: true,
432
+ promptPrefix: true,
433
+ examples: true,
434
+ temperature: true,
435
+ maxOutputTokens: true,
436
+ topP: true,
437
+ topK: true,
438
+ })
439
+ .transform((obj) => {
440
+ const isGeminiPro = obj?.model?.toLowerCase()?.includes('gemini-pro');
441
+
442
+ const maxOutputTokensMax = isGeminiPro
443
+ ? defaults.maxOutputTokens.maxGeminiPro
444
+ : defaults.maxOutputTokens.max;
445
+ const maxOutputTokensDefault = isGeminiPro
446
+ ? defaults.maxOutputTokens.defaultGeminiPro
447
+ : defaults.maxOutputTokens.default;
448
+
449
+ let maxOutputTokens = obj.maxOutputTokens ?? maxOutputTokensDefault;
450
+ maxOutputTokens = Math.min(maxOutputTokens, maxOutputTokensMax);
451
+
452
+ return {
453
+ ...obj,
454
+ model: obj.model ?? defaults.model.default,
455
+ modelLabel: obj.modelLabel ?? null,
456
+ promptPrefix: obj.promptPrefix ?? null,
457
+ examples: obj.examples ?? [{ input: { content: '' }, output: { content: '' } }],
458
+ temperature: obj.temperature ?? defaults.temperature.default,
459
+ maxOutputTokens,
460
+ topP: obj.topP ?? defaults.topP.default,
461
+ topK: obj.topK ?? defaults.topK.default,
462
+ };
463
+ })
464
+ .catch(() => ({
465
+ model: defaults.model.default,
466
+ modelLabel: null,
467
+ promptPrefix: null,
468
+ examples: [{ input: { content: '' }, output: { content: '' } }],
469
+ temperature: defaults.temperature.default,
470
+ maxOutputTokens: defaults.maxOutputTokens.default,
471
+ topP: defaults.topP.default,
472
+ topK: defaults.topK.default,
473
+ }));
474
+ };
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from './config';
4
4
  export * from './file-config';
5
5
  /* schema helpers */
6
6
  export * from './parsers';
7
+ export * from './generate';
7
8
  /* types (exports schemas from `./types` as they contain needed in other defs) */
8
9
  export * from './types';
9
10
  export * from './types/assistants';
@@ -6,13 +6,13 @@ import {
6
6
  UseMutationResult,
7
7
  QueryObserverResult,
8
8
  } from '@tanstack/react-query';
9
- import * as t from '../types';
10
- import * as s from '../schemas';
11
- import * as m from '../types/mutations';
12
- import { defaultOrderQuery } from '../config';
9
+ import { defaultOrderQuery, initialModelsConfig } from '../config';
13
10
  import * as dataService from '../data-service';
14
- import request from '../request';
11
+ import * as m from '../types/mutations';
15
12
  import { QueryKeys } from '../keys';
13
+ import request from '../request';
14
+ import * as s from '../schemas';
15
+ import * as t from '../types';
16
16
 
17
17
  export const useAbortRequestWithMessage = (): UseMutationResult<
18
18
  void,
@@ -211,10 +211,11 @@ export const useGetModelsQuery = (
211
211
  config?: UseQueryOptions<t.TModelsConfig>,
212
212
  ): QueryObserverResult<t.TModelsConfig> => {
213
213
  return useQuery<t.TModelsConfig>([QueryKeys.models], () => dataService.getModels(), {
214
- staleTime: Infinity,
214
+ initialData: initialModelsConfig,
215
215
  refetchOnWindowFocus: false,
216
216
  refetchOnReconnect: false,
217
217
  refetchOnMount: false,
218
+ staleTime: Infinity,
218
219
  ...config,
219
220
  });
220
221
  };