Skip to content

🧭 feat: Gate Config Behaviour on the Server's Admin Config Contract and Accept Dotted Keys - #173

Merged
danny-avila merged 3 commits into
mainfrom
pr/config-meta-gating
Oct 24, 2026
Merged

danny-avila merged 3 commits into
mainfrom
pr/config-meta-gating

Conversation

@danny-avila

@danny-avila danny-avila commented Oct 23, 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

LibreChat now describes its admin config API in GET /api/admin/config/meta: per-feature flags, limits and the write rules it enforces. Until now the panel could not tell which behaviours a server has, so it hard-coded the oldest behaviour everywhere. This PR adds the gating layer the following config work builds on, uses it to accept dotted record keys on servers that keep them, and fixes a set of smaller config issues.

Admin config contract

  • getAdminConfigMetaFn reads the meta route through the existing admin API client. The reply is parsed leniently: a part that is missing or malformed falls back on its own, so a fork or a newer server cannot break the page.
  • A server without the route (404), a caller who may not read it (401/403), a failed request or an unreadable reply all get the bundled contract: every flag off, and the limits and rules LibreChat enforced before it reported them. Every behaviour then stays as it is today.
  • ConfigContractProvider (mounted once in the app layout) and the useConfigContract / useConfigFeatures hooks expose features, limits and rules. Outside the provider, and until the contract loads, components get the bundled contract, so they need no query client.
  • The save path reads the contract on the server next to the target's layers and plans with its features. PATCH batches are now sized by the server's maxPatchEntries instead of a hard-coded 100.

Dotted record keys (dottedKeys)
On a server that keeps dotted keys, Bedrock inference profile IDs, Azure model names, MIME types and dotted header names can be saved:

  • The key/value, record and entry editors accept dotted keys. They still refuse $-prefixed keys and __proto__/constructor/prototype, which the server rejects with CONFIG_UNSAFE_KEY, and say so.
  • The save pre-flight (RESERVED_KEY) and the YAML import do the same. In an import:
    • A record of plain values that holds a dotted key (headers, inference profiles, MIME types) is written whole, at its own path, and merged into the target's own record, so keys the YAML leaves out stay. The preview merges it into the record the target has now (Base: the effective config; a profile: its own record), so a librechat.yaml key in the same record is not listed as removed.
    • A reserved key in such a record ($where, __proto__, constructor, prototype) is left out of the record and listed with the reserved-key reason. The rest of the record, dotted keys included, is still imported. A prototype name anywhere else in the import is now listed with that reason too, instead of being dropped silently.
    • A record of setting groups (MCP servers, file config endpoints) is still imported field by field, merged into each stored entry. A dotted name in it is left out with its own reason, since no field path can address it.
    • A dotted key where the schema expects a setting name (top level or a fixed-shape object) is left out with a reason.
    • A librechat.yaml key that LibreChat merges back into an imported record (a YAML MCP header next to an imported one) is the merge the import asks for. The result no longer counts it as not imported or warns that it can't be removed.
  • Objects with a dotted key are treated as leaves when the panel flattens config trees (override paths, write planning, verification), so no stored path splits such a key. This also stops a false "not applied" warning after saving a custom endpoint whose headers contain a dot.
  • Review & save still lists each key of such a record as its own row, and the row keeps the key whole (its path's segments, not a dot-joined path). Revert follows those segments: it drops an added key, puts back a removed or changed one, and leaves the record's other keys as they are. This also covers dotted header names on custom endpoints. A dotted key's text never makes its row a secret: a header named tenant.apiKey shows its values, while a secret field under a dotted key stays hidden.
  • MCP server names still refuse dots, in the editor and in an import: a server's settings are saved field by field, and a field path cannot address a dotted name.
  • Inside an MCP server, Create, Duplicate and Rename write a record that holds a dotted key (headers such as x.api.version, OAuth headers, user variables such as MY.TOKEN) whole, at its field, so no write splits the key into a nested path. In the user variables editor, a change to a variable whose name has a dot writes all the variables as one record, and so does every change while that write is pending. A change to any other variable writes only that variable, so a profile doesn't pin the variables it inherits. The Create dialog names a dotted variable whole in what it still needs.
  • On an older server everything stays blocked, as today.

Fixes

  • Deleting a user: deleteUserFn no longer reports success when the server has no delete route (LibreChat ships it disabled). Only LibreChat's own "User not found" reply counts as already deleted; the page explains the unavailable route.
  • Langfuse: the connection card shows the stored key's masked preview (secretKeyPreview; the field it read before never existed). The section is hidden when LibreChat reports the admin cannot use the connection settings (langfuseConnectionAccess), with the fanout flag as the fallback for older servers.
  • Resets and tombstones: the save snapshot now includes the target's tombstones, and removing an override never widens to a parent that has another tombstone under it. LibreChat drops the tombstones under a removed path, so the wider removal used to show a hidden value again.
  • Backend issues without parameters: the admin API reports issue codes only, so a code whose message needs a parameter ("must be at least {{minimum}}") is now worded without it ("is too small") instead of showing the placeholder. dotted_key and unknown_section get their own wording.
  • Global overrides: titleTiming and streamRate are marked as shadowed by endpoints.all, which wins for them per field.
  • Startup-only settings are shown read-only with an explanation: endpoints.agents.eventDriven, subagentActivity, toolApproval.hooks, the background-task batching and shutdown settings, and MCP catalog recovery's state limits and fence retry worker. Each is checked in LibreChat source: it is read once at startup, from librechat.yaml only. catalogRecovery.authorizationFenceTimeoutMs stays editable because requests read it from the merged config. Consecutive startup-only settings share one notice.
  • Web search: on a server without webSearchStoredKeys, a key or URL typed or stored in the panel is ignored (only ${VAR} references are resolved), and the credential now says so instead of "Stored encrypted".
  • Role rename: when the role has a configuration profile, the editor warns that the profile stays under the old name, where it stops applying. On a server that moves the profile with the role (roleRenameMovesProfile), it says the profile is renamed with it instead.

Checked against current main: none of these fixes were already in place.

Change Type

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

Testing

Unit and component tests, run against both a server with the contract and one without it:

  • Contract resolution: every flag off for a missing route, 401/403/500 or a thrown request; malformed parts fall back on their own; unknown flags are ignored.
  • Review & save: Revert of one dotted key (added, removed, changed, or set where nothing was stored), of a dotted header on a custom endpoint, and of a dotted entry name in a record saved whole, each leaving the other keys and changes as they are. A dotted key whose text ends in a secret name (tenant.apiKey, us.secretKey) shows its values, and a secret field under a dotted key stays hidden.
  • Planner and save path: a dotted record is sent whole with dottedKeys and blocked without it; $ keys are refused either way; PATCHes are sized by the server's limit; a reset stops below a stored tombstone or one the same batch writes; verification compares dotted records whole; a backend too_small reads "is too small".
  • Import planning and preview: a whole-record leaf, a reserved key left out of a whole record with its siblings still imported, the merge into the target's own record, no librechat.yaml key listed as removed, MCP servers and file config endpoints imported field by field with a dotted name left out (by schema or by value), dotted keys where a setting name belongs, and the same YAML on an older server.
  • Import result: keys LibreChat merges back are not counted as left out; a value that was not stored still is.
  • Field editors: dotted keys accepted or refused by the contract, and reserved keys always refused.
  • MCP servers: Create, Duplicate and Rename keep a dotted header and a dotted user variable whole, a duplicate on an older server keeps the record whole too (Save then refuses the key), and the Create dialog names a dotted variable whole. Editing a plain user variable next to a dotted one (inherited in a profile, or stored in Base) writes only that variable, and a pending removal of a dotted variable keeps the record whole through later edits.
  • Web search credentials, the role-rename note, startup-only notices, Langfuse visibility and preview, and deleteUserFn.

Live, against a LibreChat backend from current dev (all flags on) and against an older one without the meta route, with MongoDB checks:

  1. On the newer server, add us.anthropic.claude-sonnet-4-6 and global.anthropic.claude-opus-5-5 to Bedrock inference profiles, then Review & save. The review lists both keys, and the stored override holds them as they were typed. Reverting the us. row in the review removes only that key ("Save 1 change"), and saving stores only the global. profile. With both stored, deleting the global. profile and adding eu.meta.llama3-70b, then reverting the "Removed" row, puts the global. profile back with its value and keeps the new one pending. A dotted header added next to a plain one on Moonshot reverts the same way.
  2. Add the header x-portkey.config to a librechat.yaml custom endpoint and save: "Changes saved", with no "not applied" warning.
  3. Add a $where key: the row and the save are refused with the reserved-key message. Import a YAML whose github MCP headers hold x.api.version and $where, and whose Bedrock inference profiles hold a dotted profile and $where. The preview lists the dotted header and the profile, and lists both $where keys as not imported. The toast reads "Imported 2 values", and the stored override holds the two dotted keys and no $where.
  4. Import a YAML with two dotted inference profiles and balance.start.Balance: the profiles merge into the stored record, and the dotted balance key is listed as not imported.
  5. With Base overrides of timeout and initTimeout on the librechat.yaml MCP server cloudflare, import a YAML with a dotted server name my.server, serverInstructions for cloudflare, a sparse override (only serverInstructions) for another YAML server, and a dotted header for github, which already has a librechat.yaml header. The preview lists only the three new values and leaves my.server out with its reason; nothing is listed as removed and nothing blocks the import. After importing, the toast reads "Imported 3 values", the stored cloudflare override still holds both timeouts, github shows both headers, and no my.server card appears. Importing my.server and the github header into the USER role's profile writes only the header, with no warning. On the older server, my.server and the dotted header are left out as before and the rest imports field by field.
  6. On the older server, the same dotted key is refused as before (reverting its row in the review clears the problem, and reverting a plain key leaves the others), the web search key shows the "only environment variable references" warning, and the role rename shows the profile warning.
  7. On the older server, store balance.startBalance plus a tombstone on balance.refillAmount, then revert the starting balance: only balance.startBalance is removed and the tombstone stays.
  8. Rename a role with a profile on the newer server: the note says the profile moves, and after saving the profile is stored under the new name.
  9. On the newer server, create an MCP server with the headers X-Team and x.api.version and the user variable MY.TOKEN, then duplicate it and rename the copy, saving each time. Each stored server, read back through the admin API, keeps x.api.version and MY.TOKEN as whole keys. Changing the variable's title on the saved server stores it the same way, and reverting that change in the review clears it.
  10. On the newer server, override a Base server whose user variables are MY.TOKEN and REGION in the USER profile and change REGION's title: the profile stores only REGION, and a later Base change to MY.TOKEN shows in the profile. In Base, removing MY.TOKEN and then editing REGION saves both. A header tenant.apiKey added to Moonshot, then changed, shows its values in the review (acme-tenant → globex-tenant) instead of the secret notice.
  11. Light and dark mode were checked for the new notes and warnings.

The Users page is still disabled (it redirects home), so the user-delete fix is covered by its server-function tests only.

Test Configuration:

  • LibreChat dev with the admin config contract, and an older LibreChat without GET /api/admin/config/meta.
  • MongoDB 8.2 replica set, production-like librechat.yaml.
  • npx tsc --noEmit, npx eslint src/ e2e/, npx prettier --check on the changed files, bun run test:lint-rules, npx vitest run (2573 tests).

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

…nd Accept Dotted Keys

Adds a gating layer that reads the admin config contract (GET /api/admin/config/meta) with bundled
fallbacks for older servers, accepts dotted record keys where the server keeps them, and fixes a
set of smaller configuration issues.
@danny-avila

Copy link
Copy Markdown
Contributor Author

@codex review

Pass 1. Head: ecfa0b8

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 23, 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-23T19:50:52.027462Z 13fa47c 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 chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ecfa0b8c47

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

Comment thread src/components/configuration/sections/mcp/Editor.tsx Outdated
Comment thread src/components/configuration/review/diff.ts
@danny-avila

Copy link
Copy Markdown
Contributor Author

@codex review

Pass 2. Head: 332747f

1 similar comment
@danny-avila

Copy link
Copy Markdown
Contributor Author

@codex review

Pass 2. Head: 332747f

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull 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".

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

Comment thread src/utils/leaves.ts
Comment thread src/types/contract.ts
danny-avila added a commit that referenced this pull request Oct 23, 2026
danny-avila added a commit that referenced this pull request Oct 23, 2026
…by Whole Sections

Also merges the #173 fix that leaves reserved keys out of a whole imported record.
@danny-avila

Copy link
Copy Markdown
Contributor Author

@codex review

Pass 3. Head: 13fa47c

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

Reviewed commit: 13fa47c914

ℹ️ 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 added this pull request to stack #177 October 24, 2026 06:53
@danny-avila
danny-avila merged commit e468c85 into main Oct 24, 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