Repository navigation
⬆️ chore: Bump the LibreChat Packages to Their Zod 4 Releases and Support the New Schema - #213
Conversation
…port the New Schema librechat-data-provider 0.8.524 -> 0.8.530 and @librechat/data-schemas 0.0.74 -> 0.0.77, the newest on npm. data-provider now depends on zod ^4.6.7 and shares the panel's zod 4.6.7, so its zod 3.25.76 copy leaves the tree and the lockfile, and with it GHSA-mrhx-c7xc-m3w5. data-schemas 0.0.77 imports zod without declaring it; it resolves to the same top-level zod 4.6.7, and its main entry (whose 31 missing data-provider exports kept 0.0.75-0.0.77 out) and its /capabilities subpath load. re2js moves to 2.9.1 for data-provider's range, and follow-redirects to 1.16.2 inside axios's range for GHSA-mrwp-f3q8-3g9p, published a day ago. js-yaml stays on 4 and mongoose on 8.24.7; the overrides are unchanged. The schema walker reads zod 4's definitions (_zod.def.type, element, entries, values, checks) instead of zod 3's _def.typeName. It steps through prefault (data-provider's zod 3 style defaults) and nonoptional (an object's .required(), required even over an optional schema), reads a pipe's output unless that output only transforms, reads descriptions from zod's registry and checks from their definitions. Dumped on main and on this branch, the field tree differs only by data-provider's own changes: 323 new fields, new enum members (ephemeral, updates, redact), the chain capability gone from the agents default, and openidDiscovery's defaults, which data-provider now strips so zod 4 doesn't inject them. Validation reads zod 4 issues in the names the admin API reports: invalid_value is an enum's refusal or a literal's by the schema at the issue's path (a one-member enum included), a missing enum value is still "is required", invalid_format is invalid_string, and nested union branches and record keys (invalid_key) take their parent's path. Every parse sets reportInput, so the YAML import keeps a text value an enum doesn't list, and only when no other error remains, as before. The schema now states recursionLimit's limits, so the panel's own copy of them goes. The audit log mirrors all 39 published actions and 14 categories (a test compares them with the package), and names only the four it renders: any other action shows by its own name on a neutral badge with a generic summary. The new settings get labels, section meta (17 sections), interface groups, provider layout slots and rate limiters, and the MCP editor edits a server's list of objects (oauthToolCatalog) entry by entry. Tests follow the new schema and cover each ported piece, and the import e2e finds the new-role name field inside the wizard dialog, since two new System settings ("Skill name lookups") now match a plain "Name" label too.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Hooray! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…ns and Describe Ephemeral and Redact
|
Codex Review: Didn't find any major issues. 👍 Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Pull Request Template
Summary
Moves to the LibreChat packages built on Zod 4, now that npm has them:
librechat-data-provider0.8.524 → 0.8.530 and@librechat/data-schemas0.0.74 → 0.0.77, the newest releases (lateston npm for both). data-provider now depends onzod ^4.6.7, the same zod the panel uses, so its zod 3.25.76 copy leaves the tree and takes the last advisory of #189 (GHSA-mrhx-c7xc-m3w5) with it.The panel read Zod 3 internals in three places, which this PR ports to Zod 4: the configuration schema walker, the validation messages, and the YAML import's tolerance of enum values the bundled schema doesn't list (#198). data-schemas 0.0.76 grows the audit actions from 4 to 39, so the audit log now reads actions, categories and target types it doesn't name. data-provider 0.8.530 also adds 323 settings in 118 places, which get labels and a place in the configuration tabs, as #150 did for the last schema bump. Six of them are read only from librechat.yaml, so the panel shows them read-only.
What moved
librechat-data-provider@librechat/data-schemaszodunderlibrechat-data-providerre2js^2.9.1)follow-redirects^1.16.0)node_modules/zodis the only zod in the tree, andlibrechat-data-provider/node_modulesis gone. data-schemas 0.0.77 importszodat runtime without declaring it; Node resolves that import to the same top-levelzod4.6.7, which the panel's own"zod": "^4.6.7"puts there.AUTH_USER_DOC_CACHE_TTL_MS,chatProjectsConfigSchema, …), and none are missing from 0.8.530. The main entry and the/capabilitiessubpath both load, as ESM and as CJS.follow-redirects1.16.2 fixes GHSA-mrwp-f3q8-3g9p, which was published yesterday. It reaches the panel throughlibrechat-data-provider > axios.bun audit fixmoved only this lockfile entry: no override, nopackage.jsonchange.js-yamlstays on 4 andmongooseon 8.24.7, and theoverridesare unchanged: 0.8.530 still asks forjs-yaml ^4.3.2, and data-schemas peers onmongoose ^8.24.1.Audit, before and after
mainzod3.25.76 under data-provider (GHSA-mrhx-c7xc-m3w5),follow-redirects1.16.1 (GHSA-mrwp-f3q8-3g9p),minimatch3.1.5 / 10.2.6 (GHSA-g3hv-75wg-m249)minimatchThe
follow-redirectsandminimatchadvisories were both published on 2026-10-30, after #189. The minimatch fix needs minimatch 11. It only reaches the lint tooling: eslint asks for^3, and@typescript-eslint/typescript-estreeandeslint-plugin-import-xask for^10. So it isn't fixed here (see Not in this PR).The schema walker
src/server/config/schema.tsandsrc/utils/schema.tsread_zod.def(type,innerType,element,valueType,options,entries,values,checks) instead of Zod 3's_def.typeName. The localt.ZodSchemaLiketypes follow, so data-provider'sconfigSchematype-checks as one without a cast. Where Zod 4 works differently:prefault(the migration's stand-in for Zod 3's.default()on objects) counts as a default, likedefault..required(). Zod 4 wraps each field of an object's.required()innonoptional. The walk steps through it, and the field stays required even when it wraps an optional schema..catch()and.readonly(). The walk steps through both. A.catch()field is optional to write, since Zod 4 fills a missing value with the fallback. The real schema uses neither today..transform(),preprocessand.pipe()are all pipes. The walk reads a pipe's output, unless the output only transforms; then it reads the input. Soz.string().transform(fn).pipe(z.string().url())is still text with a URL check, as it was under Zod 3..describe()text in a registry rather than in the definition. The walk readsschema.descriptionon each wrapper and on the inner schema.greater_than/less_thanwithinclusive,number_format: safeintfor.int(),min_length/max_length/length_equals, andstring_formatwithurl,emailandregex. A format schema such asz.url()is its own check.z.enumandz.nativeEnumare bothenum, read fromentries. Numeric native enums still readenum(OFF=0 | ON=1), and their record values are stillnumber.Field tree,
mainagainst this branch. I dumped the tree, its defaults and its paths on both, and diffed them. The walker gives 1,257 fields onmain(data-provider 0.8.524) and 1,580 on this branch (0.8.530):nonoptionalstep, two fields regressed.endpoints.azureOpenAI.groups.[].groupandmodelsread as typenonoptional, and the models lost theirdeploymentName/version/assistantschildren. With the step, neither changes.mainis still there, with the same type, optionality, nullability, description, constraints, variants, discriminator and record value kinds. The exceptions are the 14 fields below, whose enums data-provider extended.packages/data-providerandpackages/data-schemaschanges since 0.8.524 (telemetry,timeouts,passkeys,mcpAppSandbox,endpoints.agents.pullRequests, …).interface.retentionModegainedephemeral(LibreChat#13811),modelSpecs.list[].preset.thinkingDisplaygainedupdates(#17100), and the 12filters.*.pii.actiongainedredact(#16295).endpoints.agents.capabilitiesno longer listschain(#16745 deprecated agent chains).registration.openidDiscovery.startupAttempts(1) andretryDelayMs(5000) are gone. data-provider's Zod 4 migration (#17442) wrapsopenidDiscoveryinpartialWithoutDefaults, so Zod 4 doesn't add those defaults on parse. The panel no longer shows them as default hints.endpoints.agents.recursionLimitandmaxRecursionLimitkeep the same constraints (int,min: 1). The schema now states them (#17395), so the panel's ownRUNTIME_NUMBER_LIMITScopy is removed. That copy's issues would now be reported twice.Validation messages
src/utils/validation.tsreads Zod 4 issues. They are worded in the codes the admin API reports (LibreChat'sgetConfigIssueCode), so a local issue and a reported one share the same wording, with one difference noted below:invalid_valueis an enum's refusal (invalid_enum_value: "must be one of …") or a literal's (invalid_literal: "must be …"). Zod 4 reports both alike. LibreChat tells them apart by the allowed values alone: more than one is an enum, one is a literal. The panel does the same, so a literal of several values (z.literal(['a', 'b'])) is an enum in both. The difference is a one-member enum (filters.*.pii.fields: [text]). The panel reads the schema node at the issue's path and keeps it an enum, as Zod 3 reported it ("must be one of "text""). LibreChat reports it as a literal ("must be "text"").invalid_formatisinvalid_string(url,email).too_small/too_bigreadoriginfor the length and item wordings, andintis "a whole number".invalid_key), paths relative to their parent. The panel adds the parent's path, then picks the closest union branch as before.reportInput: true, so an issue carries the value that failed. That value is what tells a missing value apart, and what the enum tolerance reads.classifyReportedIssue, which words the issues the admin API reports, is unchanged. It now shares the wording ofclassifyContractIssuewith the local path.I probed every leaf of the schema with wrong values (3,958 probes common to both branches) on
mainand on this branch:custom(the message is the same).The YAML import still shows Zod's raw messages, so they now read like "Invalid input: expected number, received string" instead of "Expected number, received string".
Enum tolerance on Zod 4 (#198)
#198's behavior is kept:
unknownEnumValueswith its path and the values the bundled schema lists.findUnknownEnumValuereads the refused value from the issue'sinput(the import parses withreportInput: true). It decides "enum" as the wording does. Across the probes, 108 are tolerated onmainand the same 108 on this branch, with no difference either way. The YAML cases match too: an unknown member, a number in an enum, an enum plus another error, a one-member enum list, a fixed literal, an enum inside a union, and capabilities with text or a number.Audit log
AuditActiongrows from 4 to 39 in data-schemas 0.0.76, andAuditCategoryfrom 8 to 14:src/constants/audit.tslist every published action and category.src/constants/audit.test.tscompares them, and the outcome, severity and actor mirrors, with the package's own arrays. The request filters keep these enums, since LibreChat refuses a filter value it doesn't know.actionandcategoryas text. So an entry a newer LibreChat logs, with an action or category the panel doesn't mirror, no longer fails the whole page, or the drawer of a deep-linked?entryId=.LABELED_AUDIT_ACTIONS. That covers the filter buttons, badges, labels and summaries, now typedRecord<t.LabeledAuditAction, …>.isLabeledAuditActionand the helpers take any text.role.created), on a neutral badge, with a generic summary ("Danny recorded role.created on Admins").config,mcp_server,audit_log, …) shows by its own name on a neutral dot. It used to fall through to the Base configuration icon and label.grantandpermission(AUDIT_LOG_CATEGORIES, checked against the package'sAUDIT_ACTION_CATEGORY). It asked forgrantonly, but LibreChat filespermission.insights_*underpermission, and its query needs an entry to match both the category and the action filter. So the Insights granted and Insights revoked filters never matched anything. This was already so onmain. The CSV export asks for the same categories.Actioncolumn carries the raw action, so it needs no change.The new settings
…Secondskeys now show seconds and…Byteskeys bytes, as…Msand…Minuteskeys show theirs (emailChange.tokenTTLSeconds: "900 s"). Labels no longer repeat a unit their field shows ("Request timeout (seconds)" is "Request timeout"), six of them frommainincluded ("IP window (minutes)", "Max private bytes", …). A rate such asrequestsPerFiveMinutescounts requests, so it has no unit.ephemeral: every chat is temporary, which users can't turn off, all data expires, and Langfuse traces none of them unless it traces temporary chats), and the option has that label. The content filters' description and help, and the filter summary's badge, cover Redact.projects,toolCallPreviewsandgithubCompare.fileListLimit.mcpAppSandbox.emailChange,passkeys,skillDeletion,timeouts,clientLimits,concurrency,passwordHashing,conversationList,messages,violationLog,credentialsandtelemetry.modelslist.workspaceAttachments,hostFileEditsandpullRequestsunder Code,subagentActivityunder Background, andstreamKeepaliveIntervalMs,linkedInstructionsandavatarRefreshunder Limits.displayandrefillMode, with the refill mode in the auto-refill group.promptResolve,emailChangeandemailChangeConfirmlimiters, with LibreChat's.envfallbacks. Keys the schema doesn't know are now listed under every limiter, including the two whose settings aren't in the table (twoFactorManagement,mcpApps).oauthToolCatalog) entry by entry.mcpAppSandboxis base-only through the package. data-provider 0.8.530'sBASE_ONLY_CONFIG_SECTIONSlists it, so the panel's interim copy of that entry is gone.Settings LibreChat reads only from librechat.yaml
I traced where LibreChat
devreads each of the new settings the panel now offers.mergeConfigOverridesmerges overrides into the app config's own keys (appConfig.clientLimits,appConfig.interfaceConfig, …). A reader ofappConfig.config(the raw librechat.yaml) or ofgetAppConfig({ baseOnly: true })never sees them. LibreChat'sDEPLOYMENT_ONLY_CONFIG_PATHSliststelemetry.stackTraceLimitandskillDeletion, but not these six:clientLimitsappConfig.config.clientLimitstelemetry(rum)config.telemetry(the proxy'sreq.configis a base-only read)conversationListgetAppConfig({ baseOnly: true })passwordHashingconfigureHashPool, once, in the startup checksfileConfig.skills.deletionCleanupConcurrencygetAppConfig({ baseOnly: true })interface.colorModeFromUrlThe panel lists them in
RAW_YAML_PATHS(src/constants/config.ts), with the reason each existing deployment-only entry uses:operatorfor a value read from librechat.yaml itself,startupfor one read once at boot. They apply on every server, whether or not it reports its rules, so they show the way Turnstile, rate limits and the other deployment-only settings already do: a read-only section with its own explanation, or a read-only field with the "Managed in librechat.yaml" or "reads the setting below only when it starts" notice. A field LibreChat lists inside a read-only section (telemetry.stackTraceLimit) no longer adds its own notice under the section's. Saves and imports skip them as they skip the other deployment-only settings.skillSyncstays editable with its banner: manual syncs read the merged config, and only the scheduled sync reads librechat.yaml.The rest of the new settings read the merged config. The ones I traced:
concurrency,credentials,emailChange,githubCompare,messages,passkeys,projects,timeouts,toolCallPreviewsandviolationLog.balance.displayandrefillMode; the agents endpoint'savatarRefresh,hostFileEdits,linkedInstructions,pullRequests(apart from its base-onlyprovider,host,oauthandsharedCacheSlots),statefulCodeSessions(apart fromprincipalWorkers.browserSetup),streamKeepaliveIntervalMs,toolApprovalandworkspaceAttachments; custom endpoints'customParams.reasoningCatalog*,tenantIdandtokenConfigcacheWrite1h; the OpenAI and Googlemodels;fileConfig.deletionRetryLimit,legacySearchProbeTimeoutMsandstt;fileListLimit; every newinterfacefield exceptcolorModeFromUrl;mcpServers.*.oauthPromptandoauthToolCatalog;mcpSettings.apps;speech.tts.maxInputChars.LibreChat already lists the new
skillDeletion,telemetry.stackTraceLimit,mcpSettings.oauthFailureRetentionMs, the agents'subagentActivity,eventDrivenandbackgroundTasksadditions,endpoints.catalogModelsTtlMsand the base-onlypullRequestsfields, andfiltersandmcpAppSandboxare base-only. Partly read from librechat.yaml, and left editable (see Not in this PR):fileConfig.retainedFileCleanup,vectorOwnershipTrackingStartandvectorDeletionConcurrency(the expired-file sweep reads the boot config), andstatefulCodeSessions.principalWorkers.browserSetup(only itsenabled: falseis read per user).Tests
Only tests whose fixtures or expected values the new schema changes were edited, plus Zod 4 test harnesses:
src/server/config.test.tsandsrc/server/config/schema.test.tsbuild their schemas withzoddirectly, instead of the zod 3 copy they used to resolve through data-provider, and checkschemaType()instead of_def.typeName. One.default({ limit: 20 })object default became.prefault(...): Zod 4's.default()takes the output type.registration.openidDiscoverynow has no defaults.interface.retentionModenow listsephemeral, both inunknownEnumValuesand in the wizard's notice ("this panel lists all, temporary, ephemeral")./expected array/i, because Zod 4 writes "Invalid input: expected array, received string".interface.replyNotifications.soundand theconversationListsection are now known, so the unknown-key examples use other names.validateFieldValue's expected message is now Zod 4's wording. It has no production caller.displayandrefillMode.promptSync, whichTLangfuseConnectionStatusnow requires..required(),prefault,.catch(),.readonly(), pipes, transforms,preprocess, lazy schemas, registry descriptions, every check kind, native enums and multi-value literals, plus a check that no wrapper type (catchandreadonlyincluded) is left unread in the real schema.findUnknownEnumValue, and the import keeping a one-member enum's text while still failing a literal.?entryId=, thegrantandpermissioncategories asked for with an Insights action filter, the categories against the package, unmirrored actions and target types on neutral badges, and the drawer without an empty Capability row.nonoptionalfrom the wrappers, reading every pipe's input, dropping the schema lookup for enums, dropping the union path prefix, parsing withoutreportInput(in validation or in the import), or droppingprefaultfrom the defaults each makes 1 to 9 of these tests fail. So does each fix of this round reverted on its own: the read-only list, the notice inside a read-only section, the response parser's enums, thegrant-only category, the Base fallback for target types, the empty Capability row, the multi-value literal, the Seconds and Bytes rules, the unitless rate per minutes, the shortened labels, the Redact badge, the Every chat temporary label, andcatch/readonly.Change Type
Testing
Every check was run on the final branch, and each exited 0:
bun install --frozen-lockfile. Thereact-movablepatch still applies.npx tsc --noEmitnpx eslint src/ --max-warnings 0, andnpx eslint e2e/ --max-warnings 0.bun run test:lint-rules: 19 of 19 pass.npx vitest run --maxWorkers=3: 183 files and 3,979 tests pass.bun run build: no Node-only module reaches the client bundle (noasync_hooks,winston,mongoose,meilisearch,jsonwebtokenornode:import). The onecryptoexternalization warning from data-provider is the same onmain.bun audit, before and after, as in the table above.Client bundle,
mainagainst this branch:mainthe client shipped both zods: Zod 3's "Invalid enum value. Expected" is inmain's bundle and not in this branch's. The growth is data-provider 0.8.530 itself (its dist grew by about 290 KB unminified) and 17 KB of new translations.hooks(1,531.3 → 1,564.0 kB) andgrants(33.1 → 33.4 kB) change by more than 0.1 kB.Playwright e2e against
e2e/mock-backend.mjs, the whole suite: 127 passed, 164 skipped (the live-only specs), none failed, exit 0.Live, against LibreChat
devon Zod 4. The backend ran LibreChatdevat e70cec8 (data-provider 0.8.530, byte-identical to the npm package), with the stack's shared production-like librechat.yaml and an empty config database.Every failure fails the same way on
main:titleResolution: field.Two more fail against this backend, the same way on
main: the Azure groups journey (providers.spec.ts:79; the backend reportsendpointRederive: false), and the profile spec's keyboard journey (profiles.spec.ts:97), whose highlighted-option locator also matched the page's native selects (3 onmain, 6 on this branch with the new System selects). That locator is fixed here: it now looks inside the profile selector's list. The three tests that didn't run are the serial journeys after failures.After this round's changes, against the same backend from an empty database,
profiles.spec.tsanda11y/config.spec.ts: 80 passed, 1 failed. Both profile journeys pass. The one failure is the 390 px System baseline above, with the same 15,718 pixels. The other eight baselines pass. With the old locator put back, the keyboard journey fails again: it matches 6 options (5 native selects' and the profile selector's).The panel's walker gives the same 1,580 fields and defaults on the bundled schema as on LibreChat
dev's ownconfigSchema. Its audit mirrors matchdevexactly (39 actions, 14 categories).Checked by hand against that backend, on this branch's production build:
ignored: []:interface.retentionMode: ephemeral, OpenAI'sstreamRate, the agents'recursionLimitandmaxRecursionLimit,emailChange.tokenTTLSecondsandenabled,passkeys.perUserMax,timeouts.ragMs,concurrency.skillNameLookups, and an MCP server'soauthToolCatalogentry.interface.retentionMode: somedayshows "Retention mode must be one of …", and a model spec with an unknown endpoint is refused.stackTraceLimit.permission.insights_assigned, an unknownpermission.workflow_sharedon anmcp_servertarget, and an unknownworkflow.started(categoryworkflow) on aconfigtarget.mcp_serveron a neutral dot.workflow.startedentry, outside the page's categories, opens by?entryId=with itsconfigtarget.Test Configuration:
vite build+bun server.ts) over plain HTTP.devat e70cec8 (data-provider 0.8.530, Zod 4), with the stack's shared production-like librechat.yaml.Not in this PR
minimatch(GHSA-g3hv-75wg-m249). The fix needs minimatch 11, and the lint packages that pull it in ask for 3.x and 10.x. It only reaches the lint tooling.dev, and fail the same way onmain: the provider title banner (D11,titleResolution: field), the stored-key rename (CE-1, the backend moves the key), the Azure groups journey (providers.spec.ts:79,endpointRederive: false), and the 390 px System baseline. Updating them belongs with the backend change that moved them.DEPLOYMENT_ONLY_CONFIG_PATHS, so writes skip them andGET /api/admin/config/metareports them. The panel'sRAW_YAML_PATHScan go once it does. Also upstream: AppService never copiesfileListLimitinto the app config, so librechat.yaml's value never applies (overrides do).fileConfig.retainedFileCleanup,vectorOwnershipTrackingStartandvectorDeletionConcurrencyapply to deletions users make, but not to the expired-file sweep, and onlyenabled: falseofstatefulCodeSessions.principalWorkers.browserSetupis read per user. A banner likeskillSync's is a follow-up.preset.codeDecisionListedandpreset.codeWorkspaces[].agentIds/checkoutcome from the conversation schema the preset derives from. LibreChat drops them when it applies a spec. They stay editable, as the other conversation-only preset fields do.interface.theme(a bundled theme's name or a theme definition) and the two JSON Schematypefields undermcpServers.*.oauthToolCatalog[].inputSchema.properties. A purpose-built control for each is a follow-up.TLangfuseConnectionStatusnow carriespromptSync. The Langfuse card doesn't show or edit it yet.…_deschelp text yet.Checklist