mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-04-03 06:17:21 +02:00
🧹 refactor: Tighten Config Schema Typing and Remove Deprecated Fields (#12452)
* refactor: Remove deprecated and unused fields from endpoint schemas - Remove summarize, summaryModel from endpointSchema and azureEndpointSchema - Remove plugins from azureEndpointSchema - Remove customOrder from endpointSchema and azureEndpointSchema - Remove baseURL from all and agents endpoint schemas - Type paramDefinitions with full SettingDefinition-based schema - Clean up summarize/summaryModel references in initialize.ts and config.spec.ts * refactor: Improve MCP transport schema typing - Add defaults to transport type discriminators (stdio, websocket, sse) - Type stderr field as IOType union instead of z.any() * refactor: Add narrowed preset schema for model specs - Create tModelSpecPresetSchema omitting system/DB/deprecated fields - Update tModelSpecSchema to use the narrowed preset schema * test: Add explicit type field to MCP test fixtures Add transport type discriminator to test objects that construct MCPOptions/ParsedServerConfig directly, required after type field changed from optional to default in schema definitions. * chore: Bump librechat-data-provider to 0.8.404 * refactor: Tighten z.record(z.any()) fields to precise value types - Type headers fields as z.record(z.string()) in endpoint, assistant, and azure schemas - Type addParams as z.record(z.union([z.string(), z.number(), z.boolean(), z.null()])) - Type azure additionalHeaders as z.record(z.string()) - Type memory model_parameters as z.record(z.union([z.string(), z.number(), z.boolean()])) - Type firecrawl changeTrackingOptions.schema as z.record(z.string()) * refactor: Type supportedMimeTypes schema as z.array(z.string()) Replace z.array(z.any()).refine() with z.array(z.string()) since config input is always strings that get converted to RegExp via convertStringsToRegex() after parsing. Destructure supportedMimeTypes from spreads to avoid string[]/RegExp[] type mismatch. * refactor: Tighten enum, role, and numeric constraint schemas - Type engineSTT as enum ['openai', 'azureOpenAI'] - Type engineTTS as enum ['openai', 'azureOpenAI', 'elevenlabs', 'localai'] - Constrain playbackRate to 0.25–4 range - Type titleMessageRole as enum ['system', 'user', 'assistant'] - Add int().nonnegative() to MCP timeout and firecrawl timeout * chore: Bump librechat-data-provider to 0.8.405 * fix: Accept both string and RegExp in supportedMimeTypes schema The schema must accept both string[] (config input) and RegExp[] (post-merge runtime) since tests validate merged output against the schema. Use z.union([z.string(), z.instanceof(RegExp)]) to handle both. * refactor: Address review findings for schema tightening PR - Revert changeTrackingOptions.schema to z.record(z.unknown()) (JSON Schema is nested, not flat strings) - Remove dead contextStrategy code from BaseClient.js and cleanup.js - Extract paramDefinitionSchema to named exported constant - Add .int() constraint to columnSpan and columns - Apply consistent .int().nonnegative() to initTimeout, sseReadTimeout, scraperTimeout - Update stale stderr JSDoc to match actual accepted types - Add comprehensive tests for paramDefinitionSchema, tModelSpecPresetSchema, endpointSchema deprecated field stripping, and azureEndpointSchema * fix: Address second review pass findings - Revert supportedMimeTypesSchema to z.array(z.string()) and remove as string[] casts — fix tests to not validate merged RegExp[] output against the config input schema - Remove unused tModelSpecSchema import from test file - Consolidate duplicate '../src/schemas' imports - Add expiredAt coverage to tModelSpecPresetSchema test - Assert plugins is absent in azureEndpointSchema test - Add sync comments for engineSTT/engineTTS enum literals * refactor: Omit preset-management fields from tModelSpecPresetSchema Omit conversationId, presetId, title, defaultPreset, and order from the model spec preset schema — these are preset-management fields that don't belong in model spec configuration.
This commit is contained in:
parent
f82d4300a4
commit
0d94881c2d
16 changed files with 384 additions and 87 deletions
|
|
@ -32,7 +32,6 @@ class BaseClient {
|
|||
constructor(apiKey, options = {}) {
|
||||
this.apiKey = apiKey;
|
||||
this.sender = options.sender ?? 'AI';
|
||||
this.contextStrategy = null;
|
||||
this.currentDateString = new Date().toLocaleDateString('en-us', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
|
|
|
|||
|
|
@ -123,9 +123,6 @@ function disposeClient(client) {
|
|||
if (client.maxContextTokens) {
|
||||
client.maxContextTokens = null;
|
||||
}
|
||||
if (client.contextStrategy) {
|
||||
client.contextStrategy = null;
|
||||
}
|
||||
if (client.currentDateString) {
|
||||
client.currentDateString = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,10 +32,8 @@ function buildCustomOptions(
|
|||
customParams: endpointConfig.customParams,
|
||||
titleConvo: endpointConfig.titleConvo,
|
||||
titleModel: endpointConfig.titleModel,
|
||||
summaryModel: endpointConfig.summaryModel,
|
||||
modelDisplayLabel: endpointConfig.modelDisplayLabel,
|
||||
titleMethod: endpointConfig.titleMethod ?? 'completion',
|
||||
contextStrategy: endpointConfig.summarize ? 'summarize' : null,
|
||||
directEndpoint: endpointConfig.directEndpoint,
|
||||
titleMessageRole: endpointConfig.titleMessageRole,
|
||||
streamRate: endpointConfig.streamRate,
|
||||
|
|
|
|||
|
|
@ -1399,10 +1399,8 @@ describe('getOpenAIConfig', () => {
|
|||
dropParams: ['presence_penalty'],
|
||||
titleConvo: true,
|
||||
titleModel: 'gpt-3.5-turbo',
|
||||
summaryModel: 'gpt-3.5-turbo',
|
||||
modelDisplayLabel: 'Custom GPT-4',
|
||||
titleMethod: 'completion',
|
||||
contextStrategy: 'summarize',
|
||||
directEndpoint: true,
|
||||
titleMessageRole: 'user',
|
||||
streamRate: 25,
|
||||
|
|
@ -1417,10 +1415,8 @@ describe('getOpenAIConfig', () => {
|
|||
customParams: {},
|
||||
titleConvo: endpointConfig.titleConvo,
|
||||
titleModel: endpointConfig.titleModel,
|
||||
summaryModel: endpointConfig.summaryModel,
|
||||
modelDisplayLabel: endpointConfig.modelDisplayLabel,
|
||||
titleMethod: endpointConfig.titleMethod,
|
||||
contextStrategy: endpointConfig.contextStrategy,
|
||||
directEndpoint: endpointConfig.directEndpoint,
|
||||
titleMessageRole: endpointConfig.titleMessageRole,
|
||||
streamRate: endpointConfig.streamRate,
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ describe('ConnectionsRepository', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
mockServerConfigs = {
|
||||
server1: { url: 'http://localhost:3001' },
|
||||
server2: { command: 'test-command', args: ['--test'] },
|
||||
server1: { url: 'http://localhost:3001', type: 'sse' },
|
||||
server2: { command: 'test-command', args: ['--test'], type: 'stdio' },
|
||||
server3: { url: 'ws://localhost:8080', type: 'websocket' },
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -377,7 +377,7 @@ describe('MCPConnection Agent lifecycle – SSE', () => {
|
|||
it('reuses the same Agents across multiple requests instead of creating one per request', async () => {
|
||||
conn = new MCPConnection({
|
||||
serverName: 'test-sse',
|
||||
serverConfig: { url: server.url },
|
||||
serverConfig: { url: server.url, type: 'sse' },
|
||||
useSSRFProtection: false,
|
||||
});
|
||||
|
||||
|
|
@ -402,7 +402,7 @@ describe('MCPConnection Agent lifecycle – SSE', () => {
|
|||
it('calls Agent.close() on every registered Agent when disconnect() is called', async () => {
|
||||
conn = new MCPConnection({
|
||||
serverName: 'test-sse',
|
||||
serverConfig: { url: server.url },
|
||||
serverConfig: { url: server.url, type: 'sse' },
|
||||
useSSRFProtection: false,
|
||||
});
|
||||
|
||||
|
|
@ -417,7 +417,7 @@ describe('MCPConnection Agent lifecycle – SSE', () => {
|
|||
it('closes at least two Agents for SSE transport (eventSourceInit + fetch)', async () => {
|
||||
conn = new MCPConnection({
|
||||
serverName: 'test-sse',
|
||||
serverConfig: { url: server.url },
|
||||
serverConfig: { url: server.url, type: 'sse' },
|
||||
useSSRFProtection: false,
|
||||
});
|
||||
|
||||
|
|
@ -431,7 +431,7 @@ describe('MCPConnection Agent lifecycle – SSE', () => {
|
|||
it('does not double-close Agents when disconnect() is called twice', async () => {
|
||||
conn = new MCPConnection({
|
||||
serverName: 'test-sse',
|
||||
serverConfig: { url: server.url },
|
||||
serverConfig: { url: server.url, type: 'sse' },
|
||||
useSSRFProtection: false,
|
||||
});
|
||||
|
||||
|
|
@ -533,7 +533,7 @@ describe('MCPConnection SSE 404 handling – session-aware', () => {
|
|||
function makeConn() {
|
||||
return new MCPConnection({
|
||||
serverName: 'test-404',
|
||||
serverConfig: { url: 'http://127.0.0.1:1/sse' },
|
||||
serverConfig: { url: 'http://127.0.0.1:1/sse', type: 'sse' },
|
||||
useSSRFProtection: false,
|
||||
});
|
||||
}
|
||||
|
|
@ -599,7 +599,7 @@ describe('MCPConnection SSE stream disconnect handling', () => {
|
|||
function makeConn() {
|
||||
return new MCPConnection({
|
||||
serverName: 'test-sse-disconnect',
|
||||
serverConfig: { url: 'http://127.0.0.1:1/sse' },
|
||||
serverConfig: { url: 'http://127.0.0.1:1/sse', type: 'sse' },
|
||||
useSSRFProtection: false,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,6 +179,7 @@ describe('Environment Variable Extraction (MCP)', () => {
|
|||
describe('processMCPEnv', () => {
|
||||
it('should create a deep clone of the input object', () => {
|
||||
const originalObj: MCPOptions = {
|
||||
type: 'stdio',
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
env: {
|
||||
|
|
@ -202,6 +203,7 @@ describe('Environment Variable Extraction (MCP)', () => {
|
|||
|
||||
it('should process environment variables in env field', () => {
|
||||
const options: MCPOptions = {
|
||||
type: 'stdio',
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
env: {
|
||||
|
|
@ -252,6 +254,7 @@ describe('Environment Variable Extraction (MCP)', () => {
|
|||
|
||||
it('should not modify objects without env or headers', () => {
|
||||
const options: MCPOptions = {
|
||||
type: 'stdio',
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
timeout: 5000,
|
||||
|
|
@ -433,6 +436,7 @@ describe('Environment Variable Extraction (MCP)', () => {
|
|||
ldapId: 'ldap-user-123',
|
||||
});
|
||||
const options: MCPOptions = {
|
||||
type: 'stdio',
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
env: {
|
||||
|
|
@ -599,6 +603,7 @@ describe('Environment Variable Extraction (MCP)', () => {
|
|||
CUSTOM_VAR_2: 'custom-value-2',
|
||||
};
|
||||
const options: MCPOptions = {
|
||||
type: 'stdio',
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
env: {
|
||||
|
|
@ -674,6 +679,7 @@ describe('Environment Variable Extraction (MCP)', () => {
|
|||
PROFILE_NAME: 'production-profile',
|
||||
};
|
||||
const options: MCPOptions = {
|
||||
type: 'stdio',
|
||||
command: 'npx',
|
||||
args: [
|
||||
'-y',
|
||||
|
|
@ -734,6 +740,7 @@ describe('Environment Variable Extraction (MCP)', () => {
|
|||
UNUSED_VAR: 'unused-value',
|
||||
};
|
||||
const options: MCPOptions = {
|
||||
type: 'stdio',
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
env: {
|
||||
|
|
@ -959,6 +966,7 @@ describe('Environment Variable Extraction (MCP)', () => {
|
|||
}) as unknown as IUser;
|
||||
|
||||
const options: MCPOptions = {
|
||||
type: 'stdio',
|
||||
command: 'node',
|
||||
args: ['mcp-server.js', '--user', '{{LIBRECHAT_USER_USERNAME}}'],
|
||||
env: {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ describe('ServerConfigsCacheInMemory Integration Tests', () => {
|
|||
|
||||
// Test data
|
||||
const mockConfig1: ParsedServerConfig = {
|
||||
type: 'stdio',
|
||||
command: 'node',
|
||||
args: ['server1.js'],
|
||||
env: { TEST: 'value1' },
|
||||
|
|
@ -19,6 +20,7 @@ describe('ServerConfigsCacheInMemory Integration Tests', () => {
|
|||
};
|
||||
|
||||
const mockConfig2: ParsedServerConfig = {
|
||||
type: 'stdio',
|
||||
command: 'python',
|
||||
args: ['server2.py'],
|
||||
env: { TEST: 'value2' },
|
||||
|
|
@ -26,6 +28,7 @@ describe('ServerConfigsCacheInMemory Integration Tests', () => {
|
|||
};
|
||||
|
||||
const mockConfig3: ParsedServerConfig = {
|
||||
type: 'stdio',
|
||||
command: 'node',
|
||||
args: ['server3.js'],
|
||||
url: 'http://localhost:3000',
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "librechat-data-provider",
|
||||
"version": "0.8.403",
|
||||
"version": "0.8.405",
|
||||
"description": "data services for librechat apps",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.es.js",
|
||||
|
|
|
|||
254
packages/data-provider/specs/config-schemas.spec.ts
Normal file
254
packages/data-provider/specs/config-schemas.spec.ts
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
import {
|
||||
endpointSchema,
|
||||
paramDefinitionSchema,
|
||||
agentsEndpointSchema,
|
||||
azureEndpointSchema,
|
||||
} from '../src/config';
|
||||
import { tModelSpecPresetSchema, EModelEndpoint } from '../src/schemas';
|
||||
|
||||
describe('paramDefinitionSchema', () => {
|
||||
it('accepts a minimal definition with only key', () => {
|
||||
const result = paramDefinitionSchema.safeParse({ key: 'temperature' });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts a full definition with all fields', () => {
|
||||
const result = paramDefinitionSchema.safeParse({
|
||||
key: 'temperature',
|
||||
type: 'number',
|
||||
component: 'slider',
|
||||
default: 0.7,
|
||||
label: 'Temperature',
|
||||
range: { min: 0, max: 2, step: 0.01 },
|
||||
columns: 2,
|
||||
columnSpan: 1,
|
||||
includeInput: true,
|
||||
descriptionSide: 'right',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects columns > 4', () => {
|
||||
const result = paramDefinitionSchema.safeParse({
|
||||
key: 'test',
|
||||
columns: 5,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects columns < 1', () => {
|
||||
const result = paramDefinitionSchema.safeParse({
|
||||
key: 'test',
|
||||
columns: 0,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects non-integer columns', () => {
|
||||
const result = paramDefinitionSchema.safeParse({
|
||||
key: 'test',
|
||||
columns: 2.5,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects non-integer columnSpan', () => {
|
||||
const result = paramDefinitionSchema.safeParse({
|
||||
key: 'test',
|
||||
columnSpan: 1.5,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects negative minTags', () => {
|
||||
const result = paramDefinitionSchema.safeParse({
|
||||
key: 'test',
|
||||
minTags: -1,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects invalid descriptionSide', () => {
|
||||
const result = paramDefinitionSchema.safeParse({
|
||||
key: 'test',
|
||||
descriptionSide: 'diagonal',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects invalid type enum value', () => {
|
||||
const result = paramDefinitionSchema.safeParse({
|
||||
key: 'test',
|
||||
type: 'invalid',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects invalid component enum value', () => {
|
||||
const result = paramDefinitionSchema.safeParse({
|
||||
key: 'test',
|
||||
component: 'wheel',
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('allows type and component to be omitted (merged from defaults at runtime)', () => {
|
||||
const result = paramDefinitionSchema.safeParse({
|
||||
key: 'temperature',
|
||||
range: { min: 0, max: 2, step: 0.01 },
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).not.toHaveProperty('type');
|
||||
expect(result.data).not.toHaveProperty('component');
|
||||
});
|
||||
});
|
||||
|
||||
describe('tModelSpecPresetSchema', () => {
|
||||
it('strips system/DB fields from preset', () => {
|
||||
const result = tModelSpecPresetSchema.safeParse({
|
||||
conversationId: 'conv-123',
|
||||
presetId: 'preset-456',
|
||||
title: 'My Preset',
|
||||
defaultPreset: true,
|
||||
order: 3,
|
||||
isArchived: true,
|
||||
user: 'user123',
|
||||
messages: ['msg1'],
|
||||
tags: ['tag1'],
|
||||
file_ids: ['file1'],
|
||||
expiredAt: '2026-12-31',
|
||||
parentMessageId: 'parent1',
|
||||
model: 'gpt-4o',
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data).not.toHaveProperty('conversationId');
|
||||
expect(result.data).not.toHaveProperty('presetId');
|
||||
expect(result.data).not.toHaveProperty('title');
|
||||
expect(result.data).not.toHaveProperty('defaultPreset');
|
||||
expect(result.data).not.toHaveProperty('order');
|
||||
expect(result.data).not.toHaveProperty('isArchived');
|
||||
expect(result.data).not.toHaveProperty('user');
|
||||
expect(result.data).not.toHaveProperty('messages');
|
||||
expect(result.data).not.toHaveProperty('tags');
|
||||
expect(result.data).not.toHaveProperty('file_ids');
|
||||
expect(result.data).not.toHaveProperty('expiredAt');
|
||||
expect(result.data).not.toHaveProperty('parentMessageId');
|
||||
expect(result.data).toHaveProperty('model', 'gpt-4o');
|
||||
}
|
||||
});
|
||||
|
||||
it('strips deprecated fields', () => {
|
||||
const result = tModelSpecPresetSchema.safeParse({
|
||||
resendImages: true,
|
||||
chatGptLabel: 'old-label',
|
||||
model: 'gpt-4o',
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data).not.toHaveProperty('resendImages');
|
||||
expect(result.data).not.toHaveProperty('chatGptLabel');
|
||||
}
|
||||
});
|
||||
|
||||
it('strips frontend-only fields', () => {
|
||||
const result = tModelSpecPresetSchema.safeParse({
|
||||
greeting: 'Hello!',
|
||||
iconURL: 'https://example.com/icon.png',
|
||||
spec: 'some-spec',
|
||||
presetOverride: { model: 'other' },
|
||||
model: 'gpt-4o',
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data).not.toHaveProperty('greeting');
|
||||
expect(result.data).not.toHaveProperty('iconURL');
|
||||
expect(result.data).not.toHaveProperty('spec');
|
||||
expect(result.data).not.toHaveProperty('presetOverride');
|
||||
}
|
||||
});
|
||||
|
||||
it('preserves valid preset fields', () => {
|
||||
const result = tModelSpecPresetSchema.safeParse({
|
||||
model: 'gpt-4o',
|
||||
endpoint: EModelEndpoint.openAI,
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
maxOutputTokens: 4096,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.model).toBe('gpt-4o');
|
||||
expect(result.data.temperature).toBe(0.7);
|
||||
expect(result.data.topP).toBe(0.9);
|
||||
expect(result.data.maxOutputTokens).toBe(4096);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('endpointSchema deprecated fields', () => {
|
||||
const validEndpoint = {
|
||||
name: 'CustomEndpoint',
|
||||
apiKey: 'test-key',
|
||||
baseURL: 'https://api.example.com',
|
||||
models: { default: ['model-1'] },
|
||||
};
|
||||
|
||||
it('silently strips deprecated summarize field', () => {
|
||||
const result = endpointSchema.safeParse({
|
||||
...validEndpoint,
|
||||
summarize: true,
|
||||
summaryModel: 'gpt-4o',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data).not.toHaveProperty('summarize');
|
||||
expect(result.data).not.toHaveProperty('summaryModel');
|
||||
}
|
||||
});
|
||||
|
||||
it('silently strips deprecated customOrder field', () => {
|
||||
const result = endpointSchema.safeParse({
|
||||
...validEndpoint,
|
||||
customOrder: 5,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data).not.toHaveProperty('customOrder');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('agentsEndpointSchema', () => {
|
||||
it('does not accept baseURL', () => {
|
||||
const result = agentsEndpointSchema.safeParse({
|
||||
baseURL: 'https://example.com',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data).not.toHaveProperty('baseURL');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('azureEndpointSchema', () => {
|
||||
it('silently strips plugins field', () => {
|
||||
const result = azureEndpointSchema.safeParse({
|
||||
groups: [
|
||||
{
|
||||
group: 'test-group',
|
||||
apiKey: 'test-key',
|
||||
models: { 'gpt-4': true },
|
||||
},
|
||||
],
|
||||
plugins: true,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data).not.toHaveProperty('plugins');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -8,7 +8,6 @@ import {
|
|||
retrievalMimeTypes,
|
||||
excelFileTypes,
|
||||
excelMimeTypes,
|
||||
fileConfigSchema,
|
||||
mergeFileConfig,
|
||||
mbToBytes,
|
||||
} from '../src/file-config';
|
||||
|
|
@ -126,8 +125,6 @@ describe('mergeFileConfig', () => {
|
|||
test('merges minimal update correctly', () => {
|
||||
const result = mergeFileConfig(dynamicConfigs.minimalUpdate);
|
||||
expect(result.serverFileSizeLimit).toEqual(mbToBytes(1024));
|
||||
const parsedResult = fileConfigSchema.safeParse(result);
|
||||
expect(parsedResult.success).toBeTruthy();
|
||||
});
|
||||
|
||||
test('overrides default endpoint with full new configuration', () => {
|
||||
|
|
@ -136,8 +133,6 @@ describe('mergeFileConfig', () => {
|
|||
expect(result.endpoints.default.supportedMimeTypes).toEqual(
|
||||
expect.arrayContaining([new RegExp('^video/.*$')]),
|
||||
);
|
||||
const parsedResult = fileConfigSchema.safeParse(result);
|
||||
expect(parsedResult.success).toBeTruthy();
|
||||
});
|
||||
|
||||
test('adds new endpoint configuration correctly', () => {
|
||||
|
|
@ -147,8 +142,6 @@ describe('mergeFileConfig', () => {
|
|||
expect(result.endpoints.newEndpoint.supportedMimeTypes).toEqual(
|
||||
expect.arrayContaining([new RegExp('^application/json$')]),
|
||||
);
|
||||
const parsedResult = fileConfigSchema.safeParse(result);
|
||||
expect(parsedResult.success).toBeTruthy();
|
||||
});
|
||||
|
||||
test('disables an endpoint and sets numeric fields to 0 and empties supportedMimeTypes', () => {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { z } from 'zod';
|
|||
import type { ZodError } from 'zod';
|
||||
import type { TEndpointsConfig, TModelsConfig, TConfig } from './types';
|
||||
import { EModelEndpoint, eModelEndpointSchema, isAgentsEndpoint } from './schemas';
|
||||
import { ComponentTypes, SettingTypes, OptionTypes } from './generate';
|
||||
import { specsConfigSchema, TSpecsConfig } from './models';
|
||||
import { fileConfigSchema } from './file-config';
|
||||
import { apiBaseUrl } from './api-endpoints';
|
||||
|
|
@ -120,11 +121,11 @@ export const azureBaseSchema = z.object({
|
|||
instanceName: z.string().optional(),
|
||||
deploymentName: z.string().optional(),
|
||||
assistants: z.boolean().optional(),
|
||||
addParams: z.record(z.any()).optional(),
|
||||
addParams: z.record(z.union([z.string(), z.number(), z.boolean(), z.null()])).optional(),
|
||||
dropParams: z.array(z.string()).optional(),
|
||||
version: z.string().optional(),
|
||||
baseURL: z.string().optional(),
|
||||
additionalHeaders: z.record(z.any()).optional(),
|
||||
additionalHeaders: z.record(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type TAzureBaseSchema = z.infer<typeof azureBaseSchema>;
|
||||
|
|
@ -257,7 +258,7 @@ export const assistantEndpointSchema = baseEndpointSchema.merge(
|
|||
userIdQuery: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
headers: z.record(z.any()).optional(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -279,6 +280,7 @@ export const defaultAgentCapabilities = [
|
|||
];
|
||||
|
||||
export const agentsEndpointSchema = baseEndpointSchema
|
||||
.omit({ baseURL: true })
|
||||
.merge(
|
||||
z.object({
|
||||
/* agents specific */
|
||||
|
|
@ -305,6 +307,43 @@ export const agentsEndpointSchema = baseEndpointSchema
|
|||
|
||||
export type TAgentsEndpoint = z.infer<typeof agentsEndpointSchema>;
|
||||
|
||||
export const paramDefinitionSchema = z.object({
|
||||
key: z.string(),
|
||||
description: z.string().optional(),
|
||||
type: z.nativeEnum(SettingTypes).optional(),
|
||||
default: z.union([z.number(), z.boolean(), z.string(), z.array(z.string())]).optional(),
|
||||
showLabel: z.boolean().optional(),
|
||||
showDefault: z.boolean().optional(),
|
||||
options: z.array(z.string()).optional(),
|
||||
range: z
|
||||
.object({
|
||||
min: z.number(),
|
||||
max: z.number(),
|
||||
step: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
enumMappings: z.record(z.union([z.number(), z.boolean(), z.string()])).optional(),
|
||||
component: z.nativeEnum(ComponentTypes).optional(),
|
||||
optionType: z.nativeEnum(OptionTypes).optional(),
|
||||
columnSpan: z.number().int().nonnegative().optional(),
|
||||
columns: z.number().int().min(1).max(4).optional(),
|
||||
label: z.string().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
labelCode: z.boolean().optional(),
|
||||
placeholderCode: z.boolean().optional(),
|
||||
descriptionCode: z.boolean().optional(),
|
||||
minText: z.number().optional(),
|
||||
maxText: z.number().optional(),
|
||||
minTags: z.number().min(0).optional(),
|
||||
maxTags: z.number().min(0).optional(),
|
||||
includeInput: z.boolean().optional(),
|
||||
descriptionSide: z.enum(['top', 'right', 'bottom', 'left']).optional(),
|
||||
searchPlaceholder: z.string().optional(),
|
||||
selectPlaceholder: z.string().optional(),
|
||||
searchPlaceholderCode: z.boolean().optional(),
|
||||
selectPlaceholderCode: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const endpointSchema = baseEndpointSchema.merge(
|
||||
z.object({
|
||||
name: z.string().refine((value) => !eModelEndpointSchema.safeParse(value).success, {
|
||||
|
|
@ -319,23 +358,20 @@ export const endpointSchema = baseEndpointSchema.merge(
|
|||
fetch: z.boolean().optional(),
|
||||
userIdQuery: z.boolean().optional(),
|
||||
}),
|
||||
summarize: z.boolean().optional(),
|
||||
summaryModel: z.string().optional(),
|
||||
iconURL: z.string().optional(),
|
||||
modelDisplayLabel: z.string().optional(),
|
||||
headers: z.record(z.any()).optional(),
|
||||
addParams: z.record(z.any()).optional(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
addParams: z.record(z.union([z.string(), z.number(), z.boolean(), z.null()])).optional(),
|
||||
dropParams: z.array(z.string()).optional(),
|
||||
customParams: z
|
||||
.object({
|
||||
defaultParamsEndpoint: z.string().default('custom'),
|
||||
paramDefinitions: z.array(z.record(z.any())).optional(),
|
||||
paramDefinitions: z.array(paramDefinitionSchema).optional(),
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
customOrder: z.number().optional(),
|
||||
directEndpoint: z.boolean().optional(),
|
||||
titleMessageRole: z.string().optional(),
|
||||
titleMessageRole: z.enum(['system', 'user', 'assistant']).optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
|
|
@ -344,7 +380,6 @@ export type TEndpoint = z.infer<typeof endpointSchema>;
|
|||
export const azureEndpointSchema = z
|
||||
.object({
|
||||
groups: azureGroupConfigsSchema,
|
||||
plugins: z.boolean().optional(),
|
||||
assistants: z.boolean().optional(),
|
||||
})
|
||||
.and(
|
||||
|
|
@ -356,9 +391,6 @@ export const azureEndpointSchema = z
|
|||
titleModel: true,
|
||||
titlePrompt: true,
|
||||
titlePromptTemplate: true,
|
||||
summarize: true,
|
||||
summaryModel: true,
|
||||
customOrder: true,
|
||||
})
|
||||
.partial(),
|
||||
);
|
||||
|
|
@ -501,7 +533,8 @@ const speechTab = z
|
|||
.optional()
|
||||
.or(
|
||||
z.object({
|
||||
engineSTT: z.string().optional(),
|
||||
/** Keep in sync with STTProviders enum (defined below — cannot reference due to eval order) */
|
||||
engineSTT: z.enum(['openai', 'azureOpenAI']).optional(),
|
||||
languageSTT: z.string().optional(),
|
||||
autoTranscribeAudio: z.boolean().optional(),
|
||||
decibelValue: z.number().optional(),
|
||||
|
|
@ -514,11 +547,12 @@ const speechTab = z
|
|||
.optional()
|
||||
.or(
|
||||
z.object({
|
||||
engineTTS: z.string().optional(),
|
||||
/** Keep in sync with TTSProviders enum (defined below — cannot reference due to eval order) */
|
||||
engineTTS: z.enum(['openai', 'azureOpenAI', 'elevenlabs', 'localai']).optional(),
|
||||
voice: z.string().optional(),
|
||||
languageTTS: z.string().optional(),
|
||||
automaticPlayback: z.boolean().optional(),
|
||||
playbackRate: z.number().optional(),
|
||||
playbackRate: z.number().min(0.25).max(4).optional(),
|
||||
cacheTTS: z.boolean().optional(),
|
||||
}),
|
||||
)
|
||||
|
|
@ -864,7 +898,7 @@ export const webSearchSchema = z.object({
|
|||
searchProvider: z.nativeEnum(SearchProviders).optional(),
|
||||
scraperProvider: z.nativeEnum(ScraperProviders).optional(),
|
||||
rerankerType: z.nativeEnum(RerankerTypes).optional(),
|
||||
scraperTimeout: z.number().optional(),
|
||||
scraperTimeout: z.number().int().nonnegative().optional(),
|
||||
safeSearch: z.nativeEnum(SafeSearchTypes).default(SafeSearchTypes.MODERATE),
|
||||
firecrawlOptions: z
|
||||
.object({
|
||||
|
|
@ -873,7 +907,7 @@ export const webSearchSchema = z.object({
|
|||
excludeTags: z.array(z.string()).optional(),
|
||||
headers: z.record(z.string()).optional(),
|
||||
waitFor: z.number().optional(),
|
||||
timeout: z.number().optional(),
|
||||
timeout: z.number().int().nonnegative().optional(),
|
||||
maxAge: z.number().optional(),
|
||||
mobile: z.boolean().optional(),
|
||||
skipTlsVerification: z.boolean().optional(),
|
||||
|
|
@ -942,7 +976,7 @@ export const memorySchema = z.object({
|
|||
provider: z.string(),
|
||||
model: z.string(),
|
||||
instructions: z.string().optional(),
|
||||
model_parameters: z.record(z.any()).optional(),
|
||||
model_parameters: z.record(z.union([z.string(), z.number(), z.boolean()])).optional(),
|
||||
}),
|
||||
])
|
||||
.optional(),
|
||||
|
|
@ -1026,7 +1060,7 @@ export const configSchema = z.object({
|
|||
modelSpecs: specsConfigSchema.optional(),
|
||||
endpoints: z
|
||||
.object({
|
||||
all: baseEndpointSchema.optional(),
|
||||
all: baseEndpointSchema.omit({ baseURL: true }).optional(),
|
||||
[EModelEndpoint.openAI]: baseEndpointSchema.optional(),
|
||||
[EModelEndpoint.google]: baseEndpointSchema.optional(),
|
||||
[EModelEndpoint.anthropic]: anthropicEndpointSchema.optional(),
|
||||
|
|
|
|||
|
|
@ -442,22 +442,7 @@ export const fileConfig = {
|
|||
},
|
||||
};
|
||||
|
||||
const supportedMimeTypesSchema = z
|
||||
.array(z.any())
|
||||
.optional()
|
||||
.refine(
|
||||
(mimeTypes) => {
|
||||
if (!mimeTypes) {
|
||||
return true;
|
||||
}
|
||||
return mimeTypes.every(
|
||||
(mimeType) => mimeType instanceof RegExp || typeof mimeType === 'string',
|
||||
);
|
||||
},
|
||||
{
|
||||
message: 'Each mimeType must be a string or a RegExp object.',
|
||||
},
|
||||
);
|
||||
const supportedMimeTypesSchema = z.array(z.string()).optional();
|
||||
|
||||
export const endpointFileConfigSchema = z.object({
|
||||
disabled: z.boolean().optional(),
|
||||
|
|
@ -690,22 +675,24 @@ export function mergeFileConfig(dynamic: z.infer<typeof fileConfigSchema> | unde
|
|||
}
|
||||
|
||||
if (dynamic.ocr !== undefined) {
|
||||
const { supportedMimeTypes: ocrMimeTypes, ...ocrRest } = dynamic.ocr;
|
||||
mergedConfig.ocr = {
|
||||
...mergedConfig.ocr,
|
||||
...dynamic.ocr,
|
||||
...ocrRest,
|
||||
};
|
||||
if (dynamic.ocr.supportedMimeTypes) {
|
||||
mergedConfig.ocr.supportedMimeTypes = convertStringsToRegex(dynamic.ocr.supportedMimeTypes);
|
||||
if (ocrMimeTypes) {
|
||||
mergedConfig.ocr.supportedMimeTypes = convertStringsToRegex(ocrMimeTypes);
|
||||
}
|
||||
}
|
||||
|
||||
if (dynamic.text !== undefined) {
|
||||
const { supportedMimeTypes: textMimeTypes, ...textRest } = dynamic.text;
|
||||
mergedConfig.text = {
|
||||
...mergedConfig.text,
|
||||
...dynamic.text,
|
||||
...textRest,
|
||||
};
|
||||
if (dynamic.text.supportedMimeTypes) {
|
||||
mergedConfig.text.supportedMimeTypes = convertStringsToRegex(dynamic.text.supportedMimeTypes);
|
||||
if (textMimeTypes) {
|
||||
mergedConfig.text.supportedMimeTypes = convertStringsToRegex(textMimeTypes);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,10 +18,10 @@ const BaseOptionsSchema = z.object({
|
|||
*/
|
||||
startup: z.boolean().optional(),
|
||||
iconPath: z.string().optional(),
|
||||
timeout: z.number().optional(),
|
||||
timeout: z.number().int().nonnegative().optional(),
|
||||
/** Timeout (ms) for the long-lived SSE GET stream body before undici aborts it. Default: 300_000 (5 min). */
|
||||
sseReadTimeout: z.number().positive().optional(),
|
||||
initTimeout: z.number().optional(),
|
||||
sseReadTimeout: z.number().int().positive().optional(),
|
||||
initTimeout: z.number().int().nonnegative().optional(),
|
||||
/** Controls visibility in chat dropdown menu (MCPSelect) */
|
||||
chatMenu: z.boolean().optional(),
|
||||
/**
|
||||
|
|
@ -104,7 +104,7 @@ const BaseOptionsSchema = z.object({
|
|||
});
|
||||
|
||||
export const StdioOptionsSchema = BaseOptionsSchema.extend({
|
||||
type: z.literal('stdio').optional(),
|
||||
type: z.literal('stdio').default('stdio'),
|
||||
/**
|
||||
* The executable to run to start the server.
|
||||
*/
|
||||
|
|
@ -134,17 +134,17 @@ export const StdioOptionsSchema = BaseOptionsSchema.extend({
|
|||
return processedEnv;
|
||||
}),
|
||||
/**
|
||||
* How to handle stderr of the child process. This matches the semantics of Node's `child_process.spawn`.
|
||||
*
|
||||
* @type {import('node:child_process').IOType | import('node:stream').Stream | number}
|
||||
*
|
||||
* The default is "inherit", meaning messages to stderr will be printed to the parent process's stderr.
|
||||
* How to handle stderr of the child process.
|
||||
* Accepts: 'pipe' | 'ignore' | 'inherit' | file descriptor number.
|
||||
* Defaults to "inherit".
|
||||
*/
|
||||
stderr: z.any().optional(),
|
||||
stderr: z
|
||||
.union([z.enum(['pipe', 'ignore', 'inherit']), z.number().int().nonnegative()])
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const WebSocketOptionsSchema = BaseOptionsSchema.extend({
|
||||
type: z.literal('websocket').optional(),
|
||||
type: z.literal('websocket').default('websocket'),
|
||||
url: z
|
||||
.string()
|
||||
.transform((val: string) => extractEnvVariable(val))
|
||||
|
|
@ -161,7 +161,7 @@ export const WebSocketOptionsSchema = BaseOptionsSchema.extend({
|
|||
});
|
||||
|
||||
export const SSEOptionsSchema = BaseOptionsSchema.extend({
|
||||
type: z.literal('sse').optional(),
|
||||
type: z.literal('sse').default('sse'),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
url: z
|
||||
.string()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { z } from 'zod';
|
||||
import type { TPreset } from './schemas';
|
||||
import type { TModelSpecPreset } from './schemas';
|
||||
import {
|
||||
EModelEndpoint,
|
||||
tPresetSchema,
|
||||
tModelSpecPresetSchema,
|
||||
eModelEndpointSchema,
|
||||
AuthType,
|
||||
authTypeSchema,
|
||||
|
|
@ -11,7 +11,7 @@ import {
|
|||
export type TModelSpec = {
|
||||
name: string;
|
||||
label: string;
|
||||
preset: TPreset;
|
||||
preset: TModelSpecPreset;
|
||||
order?: number;
|
||||
default?: boolean;
|
||||
description?: string;
|
||||
|
|
@ -42,7 +42,7 @@ export type TModelSpec = {
|
|||
export const tModelSpecSchema = z.object({
|
||||
name: z.string(),
|
||||
label: z.string(),
|
||||
preset: tPresetSchema,
|
||||
preset: tModelSpecPresetSchema,
|
||||
order: z.number().optional(),
|
||||
default: z.boolean().optional(),
|
||||
description: z.string().optional(),
|
||||
|
|
|
|||
|
|
@ -635,11 +635,15 @@ export const tMessageSchema = z.object({
|
|||
calibrationRatio: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe('EMA ratio of provider-reported vs local token estimates; seeds the pruner on subsequent runs'),
|
||||
.describe(
|
||||
'EMA ratio of provider-reported vs local token estimates; seeds the pruner on subsequent runs',
|
||||
),
|
||||
encoding: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Tokenizer encoding used when this ratio was computed (e.g. "claude", "o200k_base")'),
|
||||
.describe(
|
||||
'Tokenizer encoding used when this ratio was computed (e.g. "claude", "o200k_base")',
|
||||
),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
|
@ -919,6 +923,30 @@ export const tQueryParamsSchema = tConversationSchema
|
|||
}),
|
||||
);
|
||||
|
||||
/** Narrowed preset schema for use in model specs — omits system/DB/deprecated fields */
|
||||
export const tModelSpecPresetSchema = tPresetSchema.omit({
|
||||
conversationId: true,
|
||||
presetId: true,
|
||||
title: true,
|
||||
defaultPreset: true,
|
||||
order: true,
|
||||
isArchived: true,
|
||||
user: true,
|
||||
messages: true,
|
||||
tags: true,
|
||||
file_ids: true,
|
||||
expiredAt: true,
|
||||
parentMessageId: true,
|
||||
resendImages: true,
|
||||
chatGptLabel: true,
|
||||
presetOverride: true,
|
||||
greeting: true,
|
||||
iconURL: true,
|
||||
spec: true,
|
||||
});
|
||||
|
||||
export type TModelSpecPreset = z.infer<typeof tModelSpecPresetSchema>;
|
||||
|
||||
export type TPreset = z.infer<typeof tPresetSchema>;
|
||||
|
||||
export type TSetOption = (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue