🛟 fix: Allow Empty modelSpecs.list to Unstick Admin-Panel Saves (#13036)

* 🛟 fix: Allow empty modelSpecs.list to unstick admin-panel saves

The unconditional `.min(1)` on `specsConfigSchema.list` rejected an empty
list even when `enforce: false`, leaving admin panels (which save fields
path-granularly) with no atomic way to clear the list once it had been
populated. Once an admin reached `list: [entry]` and deleted the only
entry, every subsequent save failed schema validation and the section
became stuck.

Relax the schema to `.default([])`. The `.min(1)` was added in #5218 as
part of bundled cleanup, not as a deliberate rule. Every consumer of
`modelSpecs.list` already handles the empty/undefined case (`?.list`,
`?? []`, length-checked), and `processModelSpecs` short-circuits to
`undefined` when the list is empty so the runtime treats it as "no
specs configured." No call site is load-bearing on length >= 1.

Tighten the `buildEndpointOption.js` enforce guard from
`?.list && ?.enforce` to `?.list?.length && ?.enforce`. Empty arrays
are truthy in JS, so the existing guard would have entered the enforce
branch on `list: []` and returned "No model spec selected" or "Invalid
model spec" had `processModelSpecs` ever been bypassed.

Add a runtime warn in `processModelSpecs` when `enforce: true` is
configured alongside an empty list, so operators see the resulting
"enforcement disabled" state in logs rather than silently getting a
permissive runtime.

Add coverage for the empty-list parse path in `config-schemas.spec.ts`
and for the empty-list-with-enforce branch in `buildEndpointOption.spec.js`.

* chore: update import order in config-schemas.spec.ts
This commit is contained in:
Dustin Healy 2026-05-09 08:39:15 -07:00 committed by GitHub
parent ac3600cdd7
commit 0d5c2b339a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 77 additions and 2 deletions

View file

@ -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;

View file

@ -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' }),
);
});
});