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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "librechat-data-provider",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "data services for librechat apps",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.es.js",
@@ -0,0 +1,586 @@
1
+ /* eslint-disable jest/no-conditional-expect */
2
+ import { ZodError, z } from 'zod';
3
+ import { generateDynamicSchema, validateSettingDefinitions, OptionTypes } from '../src/generate';
4
+ import type { SettingsConfiguration } from '../src/generate';
5
+
6
+ describe('generateDynamicSchema', () => {
7
+ it('should generate a schema for number settings with range', () => {
8
+ const settings: SettingsConfiguration = [
9
+ {
10
+ key: 'testNumber',
11
+ description: 'A test number setting',
12
+ type: 'number',
13
+ default: 5,
14
+ range: { min: 1, max: 10, step: 1 },
15
+ component: 'slider',
16
+ optionType: 'conversation',
17
+ columnSpan: 2,
18
+ label: 'Test Number Slider',
19
+ },
20
+ ];
21
+
22
+ const schema = generateDynamicSchema(settings);
23
+ const result = schema.safeParse({ testNumber: 6 });
24
+
25
+ expect(result.success).toBeTruthy();
26
+ expect(result['data']).toEqual({ testNumber: 6 });
27
+ });
28
+
29
+ it('should generate a schema for boolean settings', () => {
30
+ const settings: SettingsConfiguration = [
31
+ {
32
+ key: 'testBoolean',
33
+ description: 'A test boolean setting',
34
+ type: 'boolean',
35
+ default: true,
36
+ component: 'switch',
37
+ optionType: 'model', // Only if relevant to your application's context
38
+ columnSpan: 1,
39
+ label: 'Test Boolean Switch',
40
+ },
41
+ ];
42
+
43
+ const schema = generateDynamicSchema(settings);
44
+ const result = schema.safeParse({ testBoolean: false });
45
+
46
+ expect(result.success).toBeTruthy();
47
+ expect(result['data']).toEqual({ testBoolean: false });
48
+ });
49
+
50
+ it('should generate a schema for string settings', () => {
51
+ const settings: SettingsConfiguration = [
52
+ {
53
+ key: 'testString',
54
+ description: 'A test string setting',
55
+ type: 'string',
56
+ default: 'default value',
57
+ component: 'input',
58
+ optionType: 'model', // Optional and only if relevant
59
+ columnSpan: 3,
60
+ label: 'Test String Input',
61
+ placeholder: 'Enter text here...',
62
+ minText: 0, // Optional
63
+ maxText: 100, // Optional
64
+ },
65
+ ];
66
+
67
+ const schema = generateDynamicSchema(settings);
68
+ const result = schema.safeParse({ testString: 'custom value' });
69
+
70
+ expect(result.success).toBeTruthy();
71
+ expect(result['data']).toEqual({ testString: 'custom value' });
72
+ });
73
+
74
+ it('should generate a schema for enum settings', () => {
75
+ const settings: SettingsConfiguration = [
76
+ {
77
+ key: 'testEnum',
78
+ description: 'A test enum setting',
79
+ type: 'enum',
80
+ default: 'option1',
81
+ options: ['option1', 'option2', 'option3'],
82
+ enumMappings: {
83
+ option1: 'First Option',
84
+ option2: 'Second Option',
85
+ option3: 'Third Option',
86
+ },
87
+ component: 'dropdown',
88
+ columnSpan: 2,
89
+ label: 'Test Enum Dropdown',
90
+ },
91
+ ];
92
+
93
+ const schema = generateDynamicSchema(settings);
94
+ const result = schema.safeParse({ testEnum: 'option2' });
95
+
96
+ expect(result.success).toBeTruthy();
97
+ expect(result['data']).toEqual({ testEnum: 'option2' });
98
+ });
99
+
100
+ it('should fail for incorrect enum value', () => {
101
+ const settings: SettingsConfiguration = [
102
+ {
103
+ key: 'testEnum',
104
+ description: 'A test enum setting',
105
+ type: 'enum',
106
+ default: 'option1',
107
+ options: ['option1', 'option2', 'option3'],
108
+ component: 'dropdown',
109
+ },
110
+ ];
111
+
112
+ const schema = generateDynamicSchema(settings);
113
+ const result = schema.safeParse({ testEnum: 'option4' }); // This option does not exist
114
+
115
+ expect(result.success).toBeFalsy();
116
+ });
117
+ });
118
+
119
+ describe('validateSettingDefinitions', () => {
120
+ test('should throw error for Conversation optionType', () => {
121
+ const validSettings: SettingsConfiguration = [
122
+ {
123
+ key: 'themeColor',
124
+ component: 'input',
125
+ type: 'string',
126
+ default: '#ffffff',
127
+ label: 'Theme Color',
128
+ columns: 2,
129
+ columnSpan: 1,
130
+ optionType: OptionTypes.Conversation,
131
+ },
132
+ ];
133
+
134
+ expect(() => validateSettingDefinitions(validSettings)).toThrow();
135
+ });
136
+
137
+ test('should throw error for Model optionType', () => {
138
+ const validSettings: SettingsConfiguration = [
139
+ {
140
+ key: 'themeColor',
141
+ component: 'input',
142
+ type: 'string',
143
+ default: '#ffffff',
144
+ label: 'Theme Color',
145
+ columns: 2,
146
+ columnSpan: 1,
147
+ optionType: OptionTypes.Model,
148
+ },
149
+ ];
150
+
151
+ expect(() => validateSettingDefinitions(validSettings)).toThrow();
152
+ });
153
+
154
+ test('should not throw error for valid settings', () => {
155
+ const validSettings: SettingsConfiguration = [
156
+ {
157
+ key: 'themeColor',
158
+ component: 'input',
159
+ type: 'string',
160
+ default: '#ffffff',
161
+ label: 'Theme Color',
162
+ columns: 2,
163
+ columnSpan: 1,
164
+ optionType: OptionTypes.Custom,
165
+ },
166
+ {
167
+ key: 'fontSize',
168
+ component: 'slider',
169
+ type: 'number',
170
+ range: { min: 8, max: 36 },
171
+ default: 14,
172
+ columnSpan: 2,
173
+ optionType: OptionTypes.Custom,
174
+ },
175
+ ];
176
+
177
+ expect(() => validateSettingDefinitions(validSettings)).not.toThrow();
178
+ });
179
+
180
+ // Test for incorrectly configured columns
181
+ test('should throw error for invalid columns configuration', () => {
182
+ const invalidSettings: SettingsConfiguration = [
183
+ {
184
+ key: 'themeColor',
185
+ component: 'input',
186
+ type: 'string',
187
+ columns: 5,
188
+ },
189
+ ];
190
+
191
+ expect(() => validateSettingDefinitions(invalidSettings)).toThrow(ZodError);
192
+ });
193
+
194
+ test('should correctly handle columnSpan defaulting based on columns', () => {
195
+ const settingsWithColumnAdjustment: SettingsConfiguration = [
196
+ {
197
+ key: 'fontSize',
198
+ component: 'slider',
199
+ type: 'number',
200
+ columns: 4,
201
+ range: { min: 8, max: 14 },
202
+ default: 11,
203
+ optionType: OptionTypes.Custom,
204
+ },
205
+ ];
206
+
207
+ expect(() => validateSettingDefinitions(settingsWithColumnAdjustment)).not.toThrow();
208
+ });
209
+
210
+ // Test for label defaulting to key if not provided
211
+ test('label should default to key if not explicitly set', () => {
212
+ const settingsWithDefaultLabel: SettingsConfiguration = [
213
+ {
214
+ key: 'fontWeight',
215
+ component: 'dropdown',
216
+ type: 'string',
217
+ options: ['normal', 'bold'],
218
+ optionType: OptionTypes.Custom,
219
+ },
220
+ ];
221
+
222
+ expect(() => validateSettingDefinitions(settingsWithDefaultLabel)).not.toThrow();
223
+ expect(settingsWithDefaultLabel[0].label).toBe('fontWeight');
224
+ });
225
+
226
+ // Test for minText and maxText in input/textarea component
227
+ test('should throw error for negative minText or maxText', () => {
228
+ const settingsWithNegativeTextLimits: SettingsConfiguration = [
229
+ { key: 'biography', component: 'textarea', type: 'string', minText: -1 },
230
+ ];
231
+
232
+ expect(() => validateSettingDefinitions(settingsWithNegativeTextLimits)).toThrow(ZodError);
233
+ });
234
+
235
+ // Validate optionType with tConversationSchema
236
+ test('should throw error for optionType "conversation" not matching schema', () => {
237
+ const settingsWithInvalidConversationOptionType: SettingsConfiguration = [
238
+ { key: 'userAge', component: 'input', type: 'number', optionType: 'conversation' },
239
+ ];
240
+
241
+ expect(() => validateSettingDefinitions(settingsWithInvalidConversationOptionType)).toThrow(
242
+ ZodError,
243
+ );
244
+ });
245
+
246
+ // Test for columnSpan defaulting and label defaulting to key
247
+ test('columnSpan defaults based on columns and label defaults to key if not set', () => {
248
+ const settings: SettingsConfiguration = [
249
+ {
250
+ key: 'textSize',
251
+ type: 'number',
252
+ component: 'slider',
253
+ range: { min: 10, max: 20 },
254
+ columns: 4,
255
+ optionType: OptionTypes.Custom,
256
+ },
257
+ ];
258
+
259
+ validateSettingDefinitions(settings); // Perform validation which also mutates settings with default values
260
+
261
+ expect(settings[0].columnSpan).toBe(2); // Expects columnSpan to default based on columns
262
+ expect(settings[0].label).toBe('textSize'); // Expects label to default to key
263
+ });
264
+
265
+ // Test for errors thrown due to invalid columns value
266
+ test('throws error if columns value is out of range', () => {
267
+ const settings: SettingsConfiguration = [
268
+ {
269
+ key: 'themeMode',
270
+ type: 'string',
271
+ component: 'dropdown',
272
+ options: ['dark', 'light'],
273
+ columns: 5,
274
+ },
275
+ ];
276
+
277
+ expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
278
+ });
279
+
280
+ // Test range validation for slider component
281
+ test('slider component range validation', () => {
282
+ const settings: SettingsConfiguration = [
283
+ { key: 'volume', type: 'number', component: 'slider' }, // Missing range
284
+ ];
285
+
286
+ expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
287
+ });
288
+
289
+ // Test options validation for enum type in slider component
290
+ test('slider component with enum type requires at least 2 options', () => {
291
+ const settings: SettingsConfiguration = [
292
+ { key: 'color', type: 'enum', component: 'slider', options: ['red'] }, // Not enough options
293
+ ];
294
+
295
+ expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
296
+ });
297
+
298
+ // Test checkbox component options validation
299
+ test('checkbox component must have 1-2 options if options are provided', () => {
300
+ const settings: SettingsConfiguration = [
301
+ {
302
+ key: 'agreeToTerms',
303
+ type: 'boolean',
304
+ component: 'checkbox',
305
+ options: ['Yes', 'No', 'Maybe'],
306
+ }, // Too many options
307
+ ];
308
+
309
+ expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
310
+ });
311
+
312
+ // Test dropdown component options validation
313
+ test('dropdown component requires at least 2 options', () => {
314
+ const settings: SettingsConfiguration = [
315
+ { key: 'country', type: 'enum', component: 'dropdown', options: ['USA'] }, // Not enough options
316
+ ];
317
+
318
+ expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
319
+ });
320
+
321
+ // Validate minText and maxText constraints in input and textarea
322
+ test('validate minText and maxText constraints', () => {
323
+ const settings: SettingsConfiguration = [
324
+ { key: 'biography', type: 'string', component: 'textarea', minText: 10, maxText: 5 }, // Incorrect minText and maxText
325
+ ];
326
+
327
+ expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
328
+ });
329
+
330
+ // Validate optionType constraint with tConversationSchema
331
+ test('validate optionType constraint with tConversationSchema', () => {
332
+ const settings: SettingsConfiguration = [
333
+ { key: 'userAge', type: 'number', component: 'input', optionType: 'conversation' }, // No corresponding schema in tConversationSchema
334
+ ];
335
+
336
+ expect(() => validateSettingDefinitions(settings)).toThrow(ZodError);
337
+ });
338
+
339
+ // Validate correct handling of boolean settings with default values
340
+ test('correct handling of boolean settings with defaults', () => {
341
+ const settings: SettingsConfiguration = [
342
+ {
343
+ key: 'enableFeatureX',
344
+ type: 'boolean',
345
+ component: 'switch',
346
+ optionType: OptionTypes.Custom,
347
+ }, // Missing default, should default to false
348
+ ];
349
+
350
+ validateSettingDefinitions(settings); // This would populate default values where missing
351
+
352
+ expect(settings[0].default).toBe(false); // Expects default to be false for boolean without explicit default
353
+ });
354
+
355
+ // Validate that number slider without default uses middle of range
356
+ test('number slider without default uses middle of range', () => {
357
+ const settings: SettingsConfiguration = [
358
+ {
359
+ key: 'brightness',
360
+ type: 'number',
361
+ component: 'slider',
362
+ range: { min: 0, max: 100 },
363
+ optionType: OptionTypes.Custom,
364
+ }, // Missing default
365
+ ];
366
+
367
+ validateSettingDefinitions(settings); // This would populate default values where missing
368
+
369
+ expect(settings[0].default).toBe(50); // Expects default to be midpoint of range
370
+ });
371
+ });
372
+
373
+ const settingsConfiguration: SettingsConfiguration = [
374
+ {
375
+ key: 'temperature',
376
+ description:
377
+ 'Higher values = more random, while lower values = more focused and deterministic. We recommend altering this or Top P but not both.',
378
+ type: 'number',
379
+ default: 1,
380
+ range: {
381
+ min: 0,
382
+ max: 2,
383
+ step: 0.01,
384
+ },
385
+ component: 'slider',
386
+ optionType: OptionTypes.Custom,
387
+ },
388
+ {
389
+ key: 'top_p',
390
+ description:
391
+ 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We recommend altering this or temperature but not both.',
392
+ type: 'number',
393
+ default: 1,
394
+ range: {
395
+ min: 0,
396
+ max: 1,
397
+ step: 0.01,
398
+ },
399
+ component: 'slider',
400
+ optionType: OptionTypes.Custom,
401
+ },
402
+ {
403
+ key: 'presence_penalty',
404
+ description:
405
+ 'Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model\'s likelihood to talk about new topics.',
406
+ type: 'number',
407
+ default: 0,
408
+ range: {
409
+ min: -2,
410
+ max: 2,
411
+ step: 0.01,
412
+ },
413
+ component: 'slider',
414
+ optionType: OptionTypes.Custom,
415
+ },
416
+ {
417
+ key: 'frequency_penalty',
418
+ description:
419
+ 'Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model\'s likelihood to repeat the same line verbatim.',
420
+ type: 'number',
421
+ default: 0,
422
+ range: {
423
+ min: -2,
424
+ max: 2,
425
+ step: 0.01,
426
+ },
427
+ component: 'slider',
428
+ optionType: OptionTypes.Custom,
429
+ },
430
+ {
431
+ key: 'resendFiles',
432
+ description:
433
+ 'Resend all previously attached files. Note: this will increase token cost and you may experience errors with many attachments.',
434
+ type: 'boolean',
435
+ default: true,
436
+ component: 'switch',
437
+ optionType: OptionTypes.Custom,
438
+ },
439
+ {
440
+ key: 'imageDetail',
441
+ description:
442
+ 'The resolution for Vision requests. "Low" is cheaper and faster, "High" is more detailed and expensive, and "Auto" will automatically choose between the two based on the image resolution.',
443
+ type: 'enum',
444
+ default: 'auto',
445
+ options: ['low', 'high', 'auto'],
446
+ component: 'slider',
447
+ optionType: OptionTypes.Custom,
448
+ },
449
+ {
450
+ key: 'promptPrefix',
451
+ type: 'string',
452
+ default: '',
453
+ component: 'input',
454
+ optionType: OptionTypes.Custom,
455
+ placeholder: 'Set custom instructions to include in System Message. Default: none',
456
+ },
457
+ {
458
+ key: 'chatGptLabel',
459
+ type: 'string',
460
+ default: '',
461
+ component: 'input',
462
+ optionType: OptionTypes.Custom,
463
+ placeholder: 'Set a custom name for your AI',
464
+ },
465
+ ];
466
+
467
+ describe('Settings Validation and Schema Generation', () => {
468
+ // Test 1: Validate settings definitions do not throw for valid configuration
469
+ test('validateSettingDefinitions does not throw for valid configuration', () => {
470
+ expect(() => validateSettingDefinitions(settingsConfiguration)).not.toThrow();
471
+ });
472
+
473
+ test('validateSettingDefinitions throws for invalid type in settings', () => {
474
+ const settingsWithInvalidType = [
475
+ ...settingsConfiguration,
476
+ {
477
+ key: 'newSetting',
478
+ description: 'A setting with an unsupported type',
479
+ type: 'unsupportedType', // Assuming 'unsupportedType' is not supported
480
+ component: 'input',
481
+ },
482
+ ];
483
+
484
+ expect(() =>
485
+ validateSettingDefinitions(settingsWithInvalidType as SettingsConfiguration),
486
+ ).toThrow();
487
+ });
488
+
489
+ test('validateSettingDefinitions throws for missing required fields', () => {
490
+ const settingsMissingRequiredField = [
491
+ ...settingsConfiguration,
492
+ {
493
+ key: 'incompleteSetting',
494
+ type: 'number',
495
+ // Missing 'component',
496
+ },
497
+ ];
498
+
499
+ expect(() =>
500
+ validateSettingDefinitions(settingsMissingRequiredField as SettingsConfiguration),
501
+ ).toThrow();
502
+ });
503
+
504
+ test('validateSettingDefinitions throws for default value out of range', () => {
505
+ const settingsOutOfRange = [
506
+ ...settingsConfiguration,
507
+ {
508
+ key: 'rangeTestSetting',
509
+ description: 'A setting with default value out of specified range',
510
+ type: 'number',
511
+ default: 5,
512
+ range: {
513
+ min: 0,
514
+ max: 1,
515
+ },
516
+ component: 'slider',
517
+ },
518
+ ];
519
+
520
+ expect(() => validateSettingDefinitions(settingsOutOfRange as SettingsConfiguration)).toThrow();
521
+ });
522
+
523
+ test('validateSettingDefinitions throws for enum setting with incorrect default', () => {
524
+ const settingsWithIncorrectEnumDefault = [
525
+ ...settingsConfiguration,
526
+ {
527
+ key: 'enumSetting',
528
+ description: 'Enum setting with a default not in options',
529
+ type: 'enum',
530
+ default: 'unlistedOption',
531
+ options: ['option1', 'option2'],
532
+ component: 'dropdown',
533
+ },
534
+ ];
535
+
536
+ expect(() =>
537
+ validateSettingDefinitions(settingsWithIncorrectEnumDefault as SettingsConfiguration),
538
+ ).toThrow();
539
+ });
540
+
541
+ // Test 2: Generate dynamic schema and validate correct input
542
+ test('generateDynamicSchema generates a schema that validates correct input', () => {
543
+ const schema = generateDynamicSchema(settingsConfiguration);
544
+ const validInput = {
545
+ temperature: 0.5,
546
+ top_p: 0.8,
547
+ presence_penalty: 1,
548
+ frequency_penalty: -1,
549
+ resendFiles: true,
550
+ imageDetail: 'high',
551
+ promptPrefix: 'Hello, AI.',
552
+ chatGptLabel: 'My Custom AI',
553
+ };
554
+
555
+ expect(schema.parse(validInput)).toEqual(validInput);
556
+ });
557
+
558
+ // Test 3: Generate dynamic schema and catch invalid input
559
+ test('generateDynamicSchema generates a schema that catches invalid input and provides detailed errors', async () => {
560
+ const schema = generateDynamicSchema(settingsConfiguration);
561
+ const invalidInput: z.infer<typeof schema> = {
562
+ temperature: 2.5, // Out of range
563
+ top_p: -0.5, // Out of range
564
+ presence_penalty: 3, // Out of range
565
+ frequency_penalty: -3, // Out of range
566
+ resendFiles: 'yes', // Wrong type
567
+ imageDetail: 'ultra', // Invalid option
568
+ promptPrefix: 123, // Wrong type
569
+ chatGptLabel: true, // Wrong type
570
+ };
571
+
572
+ const result = schema.safeParse(invalidInput);
573
+ expect(result.success).toBeFalsy();
574
+ if (!result.success) {
575
+ const errorPaths = result.error.issues.map((issue) => issue.path.join('.'));
576
+ expect(errorPaths).toContain('temperature');
577
+ expect(errorPaths).toContain('top_p');
578
+ expect(errorPaths).toContain('presence_penalty');
579
+ expect(errorPaths).toContain('frequency_penalty');
580
+ expect(errorPaths).toContain('resendFiles');
581
+ expect(errorPaths).toContain('imageDetail');
582
+ expect(errorPaths).toContain('promptPrefix');
583
+ expect(errorPaths).toContain('chatGptLabel');
584
+ }
585
+ });
586
+ });
package/src/config.ts CHANGED
@@ -3,6 +3,7 @@ import { z } from 'zod';
3
3
  import { EModelEndpoint, eModelEndpointSchema } from './schemas';
4
4
  import { fileConfigSchema } from './file-config';
5
5
  import { FileSources } from './types/files';
6
+ import { TModelsConfig } from './types';
6
7
 
7
8
  export const defaultSocialLogins = ['google', 'facebook', 'openid', 'github', 'discord'];
8
9
 
@@ -332,6 +333,24 @@ export const defaultModels = {
332
333
  ],
333
334
  };
334
335
 
336
+ const fitlerAssistantModels = (str: string) => {
337
+ return /gpt-4|gpt-3\\.5/i.test(str) && !/vision|instruct/i.test(str);
338
+ };
339
+
340
+ const openAIModels = defaultModels[EModelEndpoint.openAI];
341
+
342
+ export const initialModelsConfig: TModelsConfig = {
343
+ initial: [],
344
+ [EModelEndpoint.openAI]: openAIModels,
345
+ [EModelEndpoint.assistants]: openAIModels.filter(fitlerAssistantModels),
346
+ [EModelEndpoint.gptPlugins]: openAIModels,
347
+ [EModelEndpoint.azureOpenAI]: openAIModels,
348
+ [EModelEndpoint.bingAI]: ['BingAI', 'Sydney'],
349
+ [EModelEndpoint.chatGPTBrowser]: ['text-davinci-002-render-sha'],
350
+ [EModelEndpoint.google]: defaultModels[EModelEndpoint.google],
351
+ [EModelEndpoint.anthropic]: defaultModels[EModelEndpoint.anthropic],
352
+ };
353
+
335
354
  export const EndpointURLs: { [key in EModelEndpoint]: string } = {
336
355
  [EModelEndpoint.openAI]: `/api/ask/${EModelEndpoint.openAI}`,
337
356
  [EModelEndpoint.bingAI]: `/api/ask/${EModelEndpoint.bingAI}`,
@@ -202,10 +202,12 @@ export const uploadAssistantAvatar = (data: m.AssistantAvatarVariables): Promise
202
202
  );
203
203
  };
204
204
 
205
- export const getFileDownload = async (userId: string, filepath: string): Promise<AxiosResponse> => {
206
- const encodedFilePath = encodeURIComponent(filepath);
207
- return request.getResponse(`${endpoints.files()}/download/${userId}/${encodedFilePath}`, {
205
+ export const getFileDownload = async (userId: string, file_id: string): Promise<AxiosResponse> => {
206
+ return request.getResponse(`${endpoints.files()}/download/${userId}/${file_id}`, {
208
207
  responseType: 'blob',
208
+ headers: {
209
+ Accept: 'application/octet-stream',
210
+ },
209
211
  });
210
212
  };
211
213