Skip to content

🧩 fix: Import and Save Enum Values the Bundled Schema Doesn't List Yet - #198

Merged
danny-avila merged 2 commits into
mainfrom
pr/redo-import-forward-compatible-enums
Oct 28, 2026
Merged

danny-avila merged 2 commits into
mainfrom
pr/redo-import-forward-compatible-enums

Conversation

@danny-avila

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

Fixes #72: a librechat.yaml written for a LibreChat release newer than the panel's bundled librechat-data-provider fails to import with Invalid enum value. The capabilities named in #72 (subagents, skills) are known to today's bundle, but the lockstep problem is still there. LibreChat dev accepts interface.retentionMode: ephemeral, the bundled 0.8.524 doesn't, and every enum member LibreChat adds breaks import again until the panel ships a bump.

This PR supersedes #112 by @dustinhealy. It reuses that PR's tolerance policy, which #112 credits to #82: only an invalid_enum_value issue whose received value is text is tolerated, and only when no other error remains. It is written fresh on today's main, which already returns the parsed YAML without schema defaults, so #112's strip-and-restore step isn't needed.

Import parse (parseConfigYaml). The schema's issues are split in one pass:

  • An invalid_enum_value issue whose received value is text is tolerated. The YAML is kept exactly as written, and each such value is reported in a new unknownEnumValues list on the parse result, with its path and the values the panel lists there.
  • Every other issue still fails the import, as before: a number where an enum belongs, a wrong type (a capability string instead of a list), a union none of whose branches fits (an unknown speech-to-text engine), the MCP shape errors, and any YAML where such an error comes with unknown values. YAML syntax errors are unchanged.

Import wizard. The review step shows a non-blocking notice naming each such value the import writes, with its path and the values the panel lists. A value in a row the admin unchecks, or inside a key the import leaves out (a librechat.yaml-only section such as Turnstile, a list entry the target hides), isn't named. The notice says:

  • the panel's schema doesn't list the value, so it is imported as written;
  • LibreChat v0.8.8 and later check it on save, accepting a value a newer version added and refusing one they don't know, such as a typo;
  • earlier releases store it without checking, so the admin should make sure it is spelled right.

Save pre-flight (planConfigChanges). Every save and import runs this pre-flight, and before this PR it refused the same values. Without this part, the wizard would accept the YAML and then block Import with "must be one of …".

  • Enum issues now carry the value they refused (received), except inside a union's branch, which still blocks as a union failure.
  • An enum issue with a text value no longer blocks field, entry or whole-section validation, so the plan writes it. That covers an imported value, and re-saving a select or capability list that already holds a value LibreChat knows but the bundled schema doesn't.
  • No other issue code is relaxed.
  • When LibreChat refuses the value, the refusal is still mapped onto the field, worded with the allowed values where the server sends issueParams.

Which LibreChat checks the value.

  • From v0.8.8 (🧾 fix: Validate Principal Config Overrides Against configSchema LibreChat#16433), LibreChat validates config writes against its own schema. It refuses an unknown value with CONFIG_OVERRIDE_INVALID and invalid_enum_value at the field's path.
  • v0.8.5 to v0.8.7 already serve the admin config API, but they only check the principal, the priority and the path. They store an unknown value as written.
  • The panel can't tell those releases from v0.8.8. None of them serves GET /api/admin/config/meta; v0.8.9-rc1 is the first release that does.
  • Blocking the value on every server without that route would also block it on v0.8.8, which checks the value itself. So the tolerance applies to every server.
  • On v0.8.5 to v0.8.7, the notice is the only check. That is why it names every such value before anything is written.

The editors already show a value they don't list: SelectField adds it as an option, and MultiEnumField (#162) keeps it as an extra checkbox. They needed no change.

Not carried over from #112. The ListField and SelectField changes: main already renders such values. Unknown keys are a separate matter: on main they are already kept and offered unchecked in the review.

Closes #72

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Testing

The unit and component tests use made-up members (retentionMode: someday, teleport in the capability list), not ephemeral, so they keep testing the same case after the panel bumps librechat-data-provider.

Unit and component tests:

  • src/server/config.test.ts, parseConfigYaml:
    • Admin panel is not allowing config import - no lockstep with LibreChat config version #72's YAML, plus an unknown capability and an unknown interface.retentionMode, imports as written. Both values are reported with their paths and the listed values.
    • A capability string instead of a list fails, and so does a number in the capability list.
    • An unknown speech-to-text engine fails, because it sits inside a union.
    • A YAML with both an unknown value and a type error fails and names only the type error, and an MCP block of the wrong shape still fails.
    • A YAML syntax error still reports its line and column, with no unknown values.
  • src/utils/changes.test.ts, the pre-flight:
    • Enum issues carry received, but a union's branch doesn't.
    • The plan writes an unknown interface.retentionMode, a whole interface section holding it (the section pass), and a capability list where another capability is toggled next to a stored unknown member.
    • An unknown reasoning_effort in a model spec preset (a keyed collection entry) is written too.
    • It still blocks a number in the capability list, a string instead of the list, the union case, a wrong type and a non-text enum value in a model spec preset.
  • src/server/config/save.test.ts: applyConfigChanges stores the unknown value against the simulated admin API. When the server refuses a value, the refusal lands on interface.retentionMode as "is not one of the allowed values", or with issueParams as must be one of "all", "temporary", and nothing is written.
  • src/components/configuration/wizard/__tests__/Preview.test.tsx:
    • The notice names each value with its path and the listed values, and says which LibreChat releases check it.
    • It leaves out a value in an unchecked row, one under a key the import skips, and one in a list entry the target hides. It isn't shown when no value is left.
  • src/components/configuration/__tests__/ImportYamlDialog.test.tsx, the real parse, preview and pre-flight together:
    • Pasting an unknown interface.retentionMode shows the notice, raises no problems, and imports { fieldPath: 'interface.retentionMode', value: 'someday' } into Base.
    • With an unknown Turnstile size beside it, the notice names only the retention mode, and Turnstile is listed as librechat.yaml-only. Unchecking Retention mode removes the notice.

Each part was reverted on its own to confirm these tests fail without it: the parse change, the pre-flight change (and the section-pass filter alone), the notice, and the notice's filter (and its skipped-key half alone).

Live check with a production build against LibreChat dev:

Result
Import interface.retentionMode: ephemeral into Base The review step names the value, "this panel lists all, temporary", with no problems. Import writes it: Base stores retentionMode: ephemeral, and the Retention mode select shows ephemeral. On main, the parse refuses this YAML with "Config validation failed" (interface.retentionMode: Invalid enum value …), as the parse test shows with this change reverted.
A mixed YAML into Base: retentionMode: ephemeral, turnstile.options.size: huge, webSearch.searchProvider: bing, teleport in the capability list The notice names 3 values: the search provider, the retention mode and teleport. Turnstile's site key and size are listed only under "2 keys won't be imported … read from librechat.yaml only". Unchecking Retention mode drops it from the notice (2 values), and the button reads "Import 2 settings".
A value LibreChat doesn't know (retentionMode: forever, and teleport in the capability list) The notice names both and Import is enabled. LibreChat refuses the write and the dialog shows "Save failed and nothing was changed — Retention mode is not one of the allowed values; Capabilities is not one of the allowed values". Nothing is stored.
Live import e2e (e2e/import.spec.ts) 6 passed

No console errors or failed requests in these runs, and the notice reads correctly in light and dark mode.

Two checks couldn't be run live as described:

  • The panel's bundled data-provider and LibreChat dev list the same agent capabilities, so there is no capability LibreChat knows and the panel doesn't. "Toggling another capability keeps the unknown member in the written list" is covered by the pre-flight test above. In the live run, the refused capability write shows the panel sending the list with teleport kept in it.
  • No LibreChat v0.8.8 backend was on hand. v0.8.8 is the release that would refuse ephemeral: it validates writes, and its data-provider doesn't list the value yet. So the refusal was checked with a value LibreChat dev doesn't know.
    • v0.8.8 answers with the same CONFIG_OVERRIDE_INVALID body: invalid_enum_value at the field's path, with no issueParams. That is the unworded case in save.test.ts.
    • The v0.8.5 to v0.8.7 behaviour (stored without a check) comes from their source, not a live run.

On a fresh database, the second test in e2e/import.spec.ts can time out before it opens the wizard. Its ensureUserProfile helper looks for "Create profile" as soon as the network is idle, before the button renders, so the USER profile is never created. Once the profile exists, all 6 pass. This PR doesn't change the spec or the helper.

Test Configuration:

  • LibreChat dev, MongoDB replica set.
  • The panel's production build (bun run build + bun server.ts).
  • npx tsc --noEmit, npx eslint src/ e2e/ --max-warnings 0, npx prettier --check on the changed files, bun run test:lint-rules, npx vitest run (3358 tests), bun run build.

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my 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

A librechat.yaml written for a LibreChat newer than the panel's bundled
librechat-data-provider failed to import with "Invalid enum value", for
example `interface.retentionMode: ephemeral` (LibreChat dev has it, the
bundled 0.8.524 does not). The import parse now tolerates an
invalid_enum_value issue whose received value is text, keeps the YAML as
written and reports each such value with its path and the values the panel
lists (unknownEnumValues). Every other issue still fails the import: a
number where an enum belongs, a wrong type, a union none of whose branches
fits, and a mixed YAML where such errors come with unknown values.

The wizard's review step names, in a non-blocking notice, each such value
the import writes and its path: one in a row left checked, and not inside a
key the import leaves out. The notice says the value is imported as
written, that LibreChat v0.8.8 and later refuse a value they don't know
when they save it, and that earlier releases store it without checking, so
the admin should check the spelling.

The save pre-flight (planConfigChanges) treats the same issues as
non-blocking, for field and entry validation and for the whole-section pass,
so the plan writes the value: an imported one, or a select or capability
list that already holds one. Enum issues carry the value they refused
(received), except inside a union's branch, which still blocks.

LibreChat validates these writes from v0.8.8 (CONFIG_OVERRIDE_INVALID), and
its refusal is still mapped onto the field, with issueParams where the
server sends them. v0.8.5 to v0.8.7 serve the admin config API without
validating values, and the panel can't tell them from v0.8.8: none of them
serves the config meta route (v0.8.9-rc1 is the first). Blocking on a
server without it would block v0.8.8, whose data-provider (0.8.527) is
ahead of the bundled one, so on v0.8.5 to v0.8.7 the notice is the only
check.

The tests use made-up members (`someday`, `teleport`), so they keep
testing the same case after a data-provider bump.

Supersedes #112 by @dustinhealy, reusing its tolerance policy (which it
took from #82), not its code.

Closes #72
@danny-avila

Copy link
Copy Markdown
Contributor Author

@codex review

Pass 1. Head: 85338f1

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 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-27T09:40:18.626691Z 85338f1 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. 👍

Reviewed commit: 85338f1efa

ℹ️ 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 00aa9d5 into main Oct 28, 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.

Admin panel is not allowing config import - no lockstep with LibreChat config version

1 participant