diff --git a/api/server/middleware/buildEndpointOption.js b/api/server/middleware/buildEndpointOption.js index 64ed8e7466..cf4f773160 100644 --- a/api/server/middleware/buildEndpointOption.js +++ b/api/server/middleware/buildEndpointOption.js @@ -48,7 +48,7 @@ async function buildEndpointOption(req, res, next) { } const appConfig = req.config; - if (appConfig.modelSpecs?.list && appConfig.modelSpecs?.enforce) { + if (appConfig.modelSpecs?.list?.length && appConfig.modelSpecs?.enforce) { /** @type {{ list: TModelSpec[] }}*/ const { list } = appConfig.modelSpecs; const { spec } = parsedBody; diff --git a/api/server/middleware/buildEndpointOption.spec.js b/api/server/middleware/buildEndpointOption.spec.js index eab5e2666b..5d93acd6bb 100644 --- a/api/server/middleware/buildEndpointOption.spec.js +++ b/api/server/middleware/buildEndpointOption.spec.js @@ -234,4 +234,34 @@ describe('buildEndpointOption - defaultParamsEndpoint parsing', () => { expect(parsedResult.maxOutputTokens).toBeUndefined(); expect(parsedResult.max_tokens).toBe(4096); }); + + it('should not enter the enforce branch when modelSpecs.list is empty', async () => { + mockGetEndpointsConfig.mockResolvedValue({}); + + const req = createReq( + { + endpoint: EModelEndpoint.openAI, + model: 'gpt-4', + }, + { + modelSpecs: { + enforce: true, + list: [], + }, + }, + ); + const res = createRes(); + const { handleError } = require('@librechat/api'); + + await buildEndpointOption(req, res, jest.fn()); + + expect(handleError).not.toHaveBeenCalledWith( + res, + expect.objectContaining({ text: 'No model spec selected' }), + ); + expect(handleError).not.toHaveBeenCalledWith( + res, + expect.objectContaining({ text: 'Invalid model spec' }), + ); + }); }); diff --git a/packages/data-provider/specs/config-schemas.spec.ts b/packages/data-provider/specs/config-schemas.spec.ts index 94f3cc8edb..14182d6f18 100644 --- a/packages/data-provider/specs/config-schemas.spec.ts +++ b/packages/data-provider/specs/config-schemas.spec.ts @@ -11,6 +11,7 @@ import { summarizationConfigSchema, } from '../src/config'; import { tModelSpecPresetSchema, EModelEndpoint } from '../src/schemas'; +import { specsConfigSchema } from '../src/models'; import { FileSources } from '../src/types/files'; describe('paramDefinitionSchema', () => { @@ -718,3 +719,42 @@ describe('summarizationTriggerSchema', () => { expect(result.success).toBe(true); }); }); + +describe('specsConfigSchema', () => { + it('accepts an empty list (defaults applied)', () => { + const result = specsConfigSchema.safeParse({ list: [] }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.list).toEqual([]); + expect(result.data.enforce).toBe(false); + expect(result.data.prioritize).toBe(true); + } + }); + + it('defaults list to [] when omitted', () => { + const result = specsConfigSchema.safeParse({}); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.list).toEqual([]); + } + }); + + it('accepts a populated list', () => { + const result = specsConfigSchema.safeParse({ + enforce: true, + list: [ + { + name: 'spec-1', + label: 'Spec 1', + preset: { endpoint: EModelEndpoint.openAI }, + }, + ], + }); + expect(result.success).toBe(true); + }); + + it('still rejects null list', () => { + const result = specsConfigSchema.safeParse({ list: null }); + expect(result.success).toBe(false); + }); +}); diff --git a/packages/data-provider/src/models.ts b/packages/data-provider/src/models.ts index 82c2042d8a..6be55effde 100644 --- a/packages/data-provider/src/models.ts +++ b/packages/data-provider/src/models.ts @@ -62,7 +62,7 @@ export const tModelSpecSchema = z.object({ export const specsConfigSchema = z.object({ enforce: z.boolean().default(false), prioritize: z.boolean().default(true), - list: z.array(tModelSpecSchema).min(1), + list: z.array(tModelSpecSchema).default([]), addedEndpoints: z.array(z.union([z.string(), eModelEndpointSchema])).optional(), }); diff --git a/packages/data-schemas/src/app/specs.ts b/packages/data-schemas/src/app/specs.ts index 77715fb3a2..4b8ce3d42e 100644 --- a/packages/data-schemas/src/app/specs.ts +++ b/packages/data-schemas/src/app/specs.ts @@ -37,6 +37,11 @@ export function processModelSpecs( } if (!list || list.length === 0) { + if (_modelSpecs.enforce) { + logger.warn( + 'modelSpecs.enforce is true but list is empty — enforcement disabled at runtime.', + ); + } return undefined; }