Skip to content

⬆️ chore: Bump the LibreChat Packages to Their Zod 4 Releases and Support the New Schema - #213

Merged
danny-avila merged 2 commits into
mainfrom
pr/librechat-zod4
Oct 31, 2026
Merged

danny-avila merged 2 commits into
mainfrom
pr/librechat-zod4

Conversation

@danny-avila

@danny-avila danny-avila commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Pull Request Template

⚠️ Before Submitting a PR, Please Review:

  • Please ensure that you have thoroughly read and understood the Contributing Docs before submitting your Pull Request.

⚠️ Documentation Updates Notice:

  • Kindly note that documentation updates are managed in this repository: librechat.ai

Summary

Moves to the LibreChat packages built on Zod 4, now that npm has them: librechat-data-provider 0.8.524 → 0.8.530 and @librechat/data-schemas 0.0.74 → 0.0.77, the newest releases (latest on npm for both). data-provider now depends on zod ^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

Package Before After
librechat-data-provider 0.8.524 0.8.530
@librechat/data-schemas 0.0.74 0.0.77
zod under librechat-data-provider 3.25.76 (its own copy) 4.6.7 (the panel's copy)
re2js 2.9.0 2.9.1 (data-provider now asks for ^2.9.1)
follow-redirects 1.16.1 1.16.2 (inside axios's ^1.16.0)
  • zod now resolves to one copy, 4.6.7. node_modules/zod is the only zod in the tree, and librechat-data-provider/node_modules is gone. data-schemas 0.0.77 imports zod at runtime without declaring it; Node resolves that import to the same top-level zod 4.6.7, which the panel's own "zod": "^4.6.7" puts there.
  • data-schemas 0.0.77 loads. Its main entry imports 113 values from data-provider. 31 of them are missing from 0.8.524 (AUTH_USER_DOC_CACHE_TTL_MS, chatProjectsConfigSchema, …), and none are missing from 0.8.530. The main entry and the /capabilities subpath both load, as ESM and as CJS.
  • follow-redirects 1.16.2 fixes GHSA-mrwp-f3q8-3g9p, which was published yesterday. It reaches the panel through librechat-data-provider > axios. bun audit fix moved only this lockfile entry: no override, no package.json change.
  • js-yaml stays on 4 and mongoose on 8.24.7, and the overrides are unchanged: 0.8.530 still asks for js-yaml ^4.3.2, and data-schemas peers on mongoose ^8.24.1.
  • No release-age floor this time. data-provider 0.8.530 was published today, so the one-day floor of ⬆️ chore: Refresh Dependencies Within Their Majors and Clear the Fixable Audit Findings #189 can't apply to it.

Audit, before and after

Findings
main 3 (2 high, 1 moderate): zod 3.25.76 under data-provider (GHSA-mrhx-c7xc-m3w5), follow-redirects 1.16.1 (GHSA-mrwp-f3q8-3g9p), minimatch 3.1.5 / 10.2.6 (GHSA-g3hv-75wg-m249)
after the bump 2 (1 high, 1 moderate): the zod finding is gone
this PR 1 moderate: minimatch

The follow-redirects and minimatch advisories 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-estree and eslint-plugin-import-x ask for ^10. So it isn't fixed here (see Not in this PR).

The schema walker

src/server/config/schema.ts and src/utils/schema.ts read _zod.def (type, innerType, element, valueType, options, entries, values, checks) instead of Zod 3's _def.typeName. The local t.ZodSchemaLike types follow, so data-provider's configSchema type-checks as one without a cast. Where Zod 4 works differently:

  • Defaults. prefault (the migration's stand-in for Zod 3's .default() on objects) counts as a default, like default.
  • .required(). Zod 4 wraps each field of an object's .required() in nonoptional. 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.
  • Pipes. .transform(), preprocess and .pipe() are all pipes. The walk reads a pipe's output, unless the output only transforms; then it reads the input. So z.string().transform(fn).pipe(z.string().url()) is still text with a URL check, as it was under Zod 3.
  • Descriptions. Zod 4 keeps .describe() text in a registry rather than in the definition. The walk reads schema.description on each wrapper and on the inner schema.
  • Checks. Checks are read from their definitions: greater_than / less_than with inclusive, number_format: safeint for .int(), min_length / max_length / length_equals, and string_format with url, email and regex. A format schema such as z.url() is its own check.
  • Enums. z.enum and z.nativeEnum are both enum, read from entries. Numeric native enums still read enum(OFF=0 | ON=1), and their record values are still number.

Field tree, main against this branch. I dumped the tree, its defaults and its paths on both, and diffed them. The walker gives 1,257 fields on main (data-provider 0.8.524) and 1,580 on this branch (0.8.530):

  • Before the nonoptional step, two fields regressed. endpoints.azureOpenAI.groups.[].group and models read as type nonoptional, and the models lost their deploymentName / version / assistants children. With the step, neither changes.
  • Removed or changed by the port: nothing. Every one of the 1,257 fields on main is 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.
  • Added: 323 fields under 118 new keys. Every new key appears in LibreChat's packages/data-provider and packages/data-schemas changes since 0.8.524 (telemetry, timeouts, passkeys, mcpAppSandbox, endpoints.agents.pullRequests, …).
  • Changed: 14 enums gained a member. interface.retentionMode gained ephemeral (LibreChat#13811), modelSpecs.list[].preset.thinkingDisplay gained updates (#17100), and the 12 filters.*.pii.action gained redact (#16295).
  • Defaults: 113 new ones, plus two schema changes.
    • endpoints.agents.capabilities no longer lists chain (#16745 deprecated agent chains).
    • registration.openidDiscovery.startupAttempts (1) and retryDelayMs (5000) are gone. data-provider's Zod 4 migration (#17442) wraps openidDiscovery in partialWithoutDefaults, so Zod 4 doesn't add those defaults on parse. The panel no longer shows them as default hints.
  • Recursion limits. endpoints.agents.recursionLimit and maxRecursionLimit keep the same constraints (int, min: 1). The schema now states them (#17395), so the panel's own RUNTIME_NUMBER_LIMITS copy is removed. That copy's issues would now be reported twice.

Validation messages

src/utils/validation.ts reads Zod 4 issues. They are worded in the codes the admin API reports (LibreChat's getConfigIssueCode), so a local issue and a reported one share the same wording, with one difference noted below:

  • invalid_value is 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"").
  • A missing value is "is required", an enum's included. Zod 3 called a missing enum value a wrong type.
  • invalid_format is invalid_string (url, email). too_small / too_big read origin for the length and item wordings, and int is "a whole number".
  • Nested issues get full paths. Zod 4 gives the issues inside a union branch, and inside a record key's failure (invalid_key), paths relative to their parent. The panel adds the parent's path, then picks the closest union branch as before.
  • A discriminator no variant has keeps Zod's wording, which is the same text in Zod 3 and Zod 4.
  • Every parse sets 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.
  • The MCP server wizard passes its transport's schema when it words a draft's issues, like every other caller. Its schemas have no one-member enum today, so its messages don't change.

classifyReportedIssue, which words the issues the admin API reports, is unchanged. It now shares the wording of classifyContractIssue with the local path.

I probed every leaf of the schema with wrong values (3,958 probes common to both branches) on main and on this branch:

  • 3,735 are identical.
  • 152 put a non-text value in an enum. Zod 3 reported a wrong type ("must be 'day' | 'month' | 'year'"). Zod 4 reports an enum refusal ("must be one of "day", "month", "year""), which still blocks.
  • 29 put a fraction where an integer with a minimum belongs. Zod 4 stops at the failed integer check: "must be a whole number", without "must be at least 1".
  • 16 hit the enum members data-provider added.
  • 12 fail a regex. They fall back to Zod's own message, which Zod 4 words "Invalid string: must match pattern …" instead of "Invalid".
  • 10 hit refinements data-provider added: Vertex AI project, region and deployment patterns, and the recursion limits.
  • 4 hit a URL check that throws. They now carry code 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:

  • The import keeps text an enum doesn't list, but only when no other error remains, and names it in unknownEnumValues with its path and the values the bundled schema lists.
  • The save writes it.
  • A number, a one-value literal, or an enum inside a union's branch still blocks. A literal of several values counts as an enum, as in the wording above; the bundled schema has none.

findUnknownEnumValue reads the refused value from the issue's input (the import parses with reportInput: true). It decides "enum" as the wording does. Across the probes, 108 are tolerated on main and 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

AuditAction grows from 4 to 39 in data-schemas 0.0.76, and AuditCategory from 8 to 14:

  • The mirrors in src/constants/audit.ts list every published action and category. src/constants/audit.test.ts compares 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.
  • The response parser reads action and category as 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=.
  • The tab names four actions: LABELED_AUDIT_ACTIONS. That covers the filter buttons, badges, labels and summaries, now typed Record<t.LabeledAuditAction, …>. isLabeledAuditAction and the helpers take any text.
  • Any other action shows as its own name (role.created), on a neutral badge, with a generic summary ("Danny recorded role.created on Admins").
  • A target type the tab has no name for (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.
  • The drawer leaves out the Capability row when an entry concerns no capability. It used to show an empty row with a lone copy button.
  • The page asks for the categories of its actions: grant and permission (AUDIT_LOG_CATEGORIES, checked against the package's AUDIT_ACTION_CATEGORY). It asked for grant only, but LibreChat files permission.insights_* under permission, 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 on main. The CSV export asks for the same categories.
  • The CSV export is LibreChat's own stream. Its Action column carries the raw action, so it needs no change.

The new settings

  • Labels: 233 field labels, 7 list-item labels, and titles and descriptions for the 17 new sections.
  • Units: …Seconds keys now show seconds and …Bytes keys bytes, as …Ms and …Minutes keys show theirs (emailChange.tokenTTLSeconds: "900 s"). Labels no longer repeat a unit their field shows ("Request timeout (seconds)" is "Request timeout"), six of them from main included ("IP window (minutes)", "Max private bytes", …). A rate such as requestsPerFiveMinutes counts requests, so it has no unit.
  • Copy for the new enum members: the retention mode's description covers Every chat temporary (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.
  • Section meta:
    • Features: projects, toolCallPreviews and githubCompare.
    • Files: fileListLimit.
    • MCP: mcpAppSandbox.
    • System: emailChange, passkeys, skillDeletion, timeouts, clientLimits, concurrency, passwordHashing, conversationList, messages, violationLog, credentials and telemetry.
  • Interface groups:
    • New groups: Appearance (theme, color mode from URL, landing heading and footer, mascot), Reply notifications, and Timing and caching.
    • Chat controls also gets the agent selector limit, composer recent files, running-chat rename and artifact undocking.
    • The analytics consent goes under Onboarding & legal, and the marketplace's retry delays under Sharing.
  • Provider layouts:
    • OpenAI and Google get a Models group for their new models list.
    • Agents places workspaceAttachments, hostFileEdits and pullRequests under Code, subagentActivity under Background, and streamKeepaliveIntervalMs, linkedInstructions and avatarRefresh under Limits.
  • Credits: the balance display and refillMode, with the refill mode in the auto-refill group.
  • Rate limits: the table gains the promptResolve, emailChange and emailChangeConfirm limiters, with LibreChat's .env fallbacks. Keys the schema doesn't know are now listed under every limiter, including the two whose settings aren't in the table (twoFactorManagement, mcpApps).
  • MCP: the server editor edits a list of objects (the new oauthToolCatalog) entry by entry.
  • mcpAppSandbox is base-only through the package. data-provider 0.8.530's BASE_ONLY_CONFIG_SECTIONS lists it, so the panel's interim copy of that entry is gone.

Settings LibreChat reads only from librechat.yaml

I traced where LibreChat dev reads each of the new settings the panel now offers. mergeConfigOverrides merges overrides into the app config's own keys (appConfig.clientLimits, appConfig.interfaceConfig, …). A reader of appConfig.config (the raw librechat.yaml) or of getAppConfig({ baseOnly: true }) never sees them. LibreChat's DEPLOYMENT_ONLY_CONFIG_PATHS lists telemetry.stackTraceLimit and skillDeletion, but not these six:

Setting Read by Shown
clientLimits the startup config, from appConfig.config.clientLimits read-only section
telemetry (rum) the startup config and the RUM proxy, from config.telemetry (the proxy's req.config is a base-only read) read-only section
conversationList the list route and the startup config, through getAppConfig({ baseOnly: true }) read-only section
passwordHashing configureHashPool, once, in the startup checks read-only section ("when it starts")
fileConfig.skills.deletionCleanupConcurrency the skill deletion service, through getAppConfig({ baseOnly: true }) read-only field
interface.colorModeFromUrl the HTML shell, once, at boot read-only field ("only when it starts")

The panel lists them in RAW_YAML_PATHS (src/constants/config.ts), with the reason each existing deployment-only entry uses: operator for a value read from librechat.yaml itself, startup for 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. skillSync stays 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:

  • Sections: concurrency, credentials, emailChange, githubCompare, messages, passkeys, projects, timeouts, toolCallPreviews and violationLog.
  • Fields: balance.display and refillMode; the agents endpoint's avatarRefresh, hostFileEdits, linkedInstructions, pullRequests (apart from its base-only provider, host, oauth and sharedCacheSlots), statefulCodeSessions (apart from principalWorkers.browserSetup), streamKeepaliveIntervalMs, toolApproval and workspaceAttachments; custom endpoints' customParams.reasoningCatalog*, tenantId and tokenConfig cacheWrite1h; the OpenAI and Google models; fileConfig.deletionRetryLimit, legacySearchProbeTimeoutMs and stt; fileListLimit; every new interface field except colorModeFromUrl; mcpServers.*.oauthPrompt and oauthToolCatalog; mcpSettings.apps; speech.tts.maxInputChars.

LibreChat already lists the new skillDeletion, telemetry.stackTraceLimit, mcpSettings.oauthFailureRetentionMs, the agents' subagentActivity, eventDriven and backgroundTasks additions, endpoints.catalogModelsTtlMs and the base-only pullRequests fields, and filters and mcpAppSandbox are base-only. Partly read from librechat.yaml, and left editable (see Not in this PR): fileConfig.retainedFileCleanup, vectorOwnershipTrackingStart and vectorDeletionConcurrency (the expired-file sweep reads the boot config), and statefulCodeSessions.principalWorkers.browserSetup (only its enabled: false is 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.ts and src/server/config/schema.test.ts build their schemas with zod directly, instead of the zod 3 copy they used to resolve through data-provider, and check schemaType() instead of _def.typeName. One .default({ limit: 20 }) object default became .prefault(...): Zod 4's .default() takes the output type. registration.openidDiscovery now has no defaults.
  • 🧩 fix: Import and Save Enum Values the Bundled Schema Doesn't List Yet #198's tests keep their assertions. Two expected values follow the schema:
    • interface.retentionMode now lists ephemeral, both in unknownEnumValues and in the wizard's notice ("this panel lists all, temporary, ephemeral").
    • The wrong-type message check reads /expected array/i, because Zod 4 writes "Invalid input: expected array, received string".
  • Schema content:
    • interface.replyNotifications.sound and the conversationList section 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.
    • The recursion-limit test now sees one issue per limit.
    • The Balance order test includes display and refillMode.
    • The Interface headings test lists the three new groups.
    • The YAML-fallback audit lists the three new fields that only the YAML editor edits (see Not in this PR).
    • The schema round-trip test samples a field's minimum and a URL for URL fields, plus six overrides for refined fields.
    • The Langfuse fixtures carry promptSync, which TLangfuseConnectionStatus now requires.
    • The Features coverage fixture holds one MCP consent resource and one read-only policy.
    • The bundled read-only sections now include the four above.
  • New tests:
    • The Zod 4 walk: .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 (catch and readonly included) is left unread in the real schema.
    • The issue wording: enum vs literal, the one-member enum, the multi-value literal, missing values, bounds, formats, union and record-key paths, and the discriminator.
    • findUnknownEnumValue, and the import keeping a one-member enum's text while still failing a literal.
    • The settings LibreChat reads only from librechat.yaml: read-only on a server that reports data-provider 0.8.530's own lists and on one without rules, each section with its reason, the two fields read-only beside editable siblings, and no second notice inside a read-only section.
    • The units: seconds and bytes from the key, none for a rate per minutes, and no label repeating a time or size unit its field shows.
    • The audit log: an entry with an unmirrored action and category on the page and by ?entryId=, the grant and permission categories 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.
    • The Redact badge, and a filter action a newer LibreChat adds by its own name; the retention options' labels.
  • Mutation check. Removing nonoptional from the wrappers, reading every pipe's input, dropping the schema lookup for enums, dropping the union path prefix, parsing without reportInput (in validation or in the import), or dropping prefault from 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, the grant-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, and catch / readonly.

Change Type

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)

Testing

Every check was run on the final branch, and each exited 0:

  • bun install --frozen-lockfile. The react-movable patch still applies.
  • npx tsc --noEmit
  • npx eslint src/ --max-warnings 0, and npx eslint e2e/ --max-warnings 0.
  • Prettier on every changed file.
  • 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 (no async_hooks, winston, mongoose, meilisearch, jsonwebtoken or node: import). The one crypto externalization warning from data-provider is the same on main.
  • bun audit, before and after, as in the table above.

Client bundle, main against this branch:

  • JavaScript: 3,738,689 → 3,772,005 bytes (+33 KB, +0.9%).
  • Zod 3 is gone from the bundle. On main the client shipped both zods: Zod 3's "Invalid enum value. Expected" is in main'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.
  • Chunks: only hooks (1,531.3 → 1,564.0 kB) and grants (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 dev on Zod 4. The backend ran LibreChat dev at 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.

Live specs: config, config-shell, import, saves, profiles, features, providers, mcp, specs, a11y/config Passed Failed Skipped Did not run
this PR 195 3 32 (by design) 3

Every failure fails the same way on main:

  • The 390 px System baseline: 15,718 pixels differ. This backend applies Base overrides before sign-in, so the social sign-in buttons are editable, and the baseline shows them read-only.
  • The provider title banner (D11): the backend now reports titleResolution: field.
  • The stored-key rename (CE-1): the backend now moves a stored key on rename.

Two more fail against this backend, the same way on main: the Azure groups journey (providers.spec.ts:79; the backend reports endpointRederive: false), and the profile spec's keyboard journey (profiles.spec.ts:97), whose highlighted-option locator also matched the page's native selects (3 on main, 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.ts and a11y/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 own configSchema. Its audit mirrors match dev exactly (39 actions, 14 categories).

Checked by hand against that backend, on this branch's production build:

  • Nine new settings saved and persisted, each with ignored: []: interface.retentionMode: ephemeral, OpenAI's streamRate, the agents' recursionLimit and maxRecursionLimit, emailChange.tokenTTLSeconds and enabled, passkeys.perUserMax, timeouts.ragMs, concurrency.skillNameLookups, and an MCP server's oauthToolCatalog entry.
  • LibreChat's refusals reach the panel: importing interface.retentionMode: someday shows "Retention mode must be one of …", and a model spec with an unknown endpoint is refused.
  • The settings read only from librechat.yaml:
    • Client limits, Telemetry and Conversation list show the lock banner with their own reason ("LibreChat sends these limits to the browser from librechat.yaml only, …"), and Password hashing shows its startup reason. None of them has an input.
    • Telemetry, opened, adds no second notice for stackTraceLimit.
    • "Color mode from URL" shows as a static value under "LibreChat reads the setting below only when it starts".
    • Skills' "Deletion cleanup concurrency" is static under "Managed in librechat.yaml", while "Import cleanup concurrency" beside it stays an input.
    • Email change and Skill sync stay editable.
  • The audit log, with synthetic entries in the throwaway database: a grant, a permission.insights_assigned, an unknown permission.workflow_shared on an mcp_server target, and an unknown workflow.started (category workflow) on a config target.
    • The page loads with 3 entries. The unknown action shows by its name on a neutral badge, and its target as mcp_server on a neutral dot.
    • Its drawer reads "Test Admin recorded permission.workflow_shared on Synthetic MCP", with no Capability row.
    • Insights granted lists the Insights entry.
    • The workflow.started entry, outside the page's categories, opens by ?entryId= with its config target.
    • No console error or failed request.

Test Configuration:

  • Bun 1.4.0, Node 24.16.0.
  • Production build (vite build + bun server.ts) over plain HTTP.
  • LibreChat backend from dev at 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.
  • Four live expectations no longer hold against LibreChat dev, and fail the same way on main: 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.
  • Upstream: LibreChat should list the six settings above in DEPLOYMENT_ONLY_CONFIG_PATHS, so writes skip them and GET /api/admin/config/meta reports them. The panel's RAW_YAML_PATHS can go once it does. Also upstream: AppService never copies fileListLimit into the app config, so librechat.yaml's value never applies (overrides do).
  • Settings LibreChat reads partly from librechat.yaml stay editable: fileConfig.retainedFileCleanup, vectorOwnershipTrackingStart and vectorDeletionConcurrency apply to deletions users make, but not to the expired-file sweep, and only enabled: false of statefulCodeSessions.principalWorkers.browserSetup is read per user. A banner like skillSync's is a follow-up.
  • Three model spec preset fields nothing reads: preset.codeDecisionListed and preset.codeWorkspaces[].agentIds / checkout come 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.
  • Three new fields use the YAML editor: interface.theme (a bundled theme's name or a theme definition) and the two JSON Schema type fields under mcpServers.*.oauthToolCatalog[].inputSchema.properties. A purpose-built control for each is a follow-up.
  • Langfuse prompt sync. TLangfuseConnectionStatus now carries promptSync. The Langfuse card doesn't show or edit it yet.
  • New settings have labels but no descriptions. The new sections have descriptions, but most new fields don't have …_desc help text yet.

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes

…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.
@danny-avila

Copy link
Copy Markdown
Contributor Author

@codex review

Pass 1. Head: 8c29d75

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-31T22:44:12.756480Z 7eb9d64 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: 8c29d75628

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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".

@danny-avila

Copy link
Copy Markdown
Contributor Author

@codex review

Pass 2. Head: 7eb9d64

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: 7eb9d64150

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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".

@danny-avila
danny-avila merged commit cf15c4d into main Oct 31, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant