🛂 feat: Add AWS Profile Support for Bedrock Credentials#10504
Conversation
- Add BEDROCK_AWS_PROFILE environment variable support - Implement AWS SDK credential provider chain for automatic refresh - Update credential loading logic to support profiles, static env vars, and user-provided credentials - Add logging for credential source transparency - Update .env.example with profile configuration documentation Follows S3 implementation pattern for credential handling. Enables users to configure AWS profiles with optional credential_process for automatic token refresh.
- Add AWS profile configuration as Option 1 (recommended for temporary credentials) - Document credential_process for automatic credential refresh - Reorganize authentication methods into three clear options with recommendations - Add examples for ~/.aws/config and ~/.aws/credentials setup - Include JSON format specification for credential_process output - Add links to AWS CLI configuration and external process documentation - Clarify when to use each authentication method (profiles vs provider chain vs static) - Add warnings about static credentials with temporary tokens Aligns documentation with new BEDROCK_AWS_PROFILE feature in LibreChat - PR: danny-avila/LibreChat#10504.
|
Issue: #10506 |
|
Please add corresponding documentation PR to https://github.com/LibreChat-AI/librechat.ai |
There was a problem hiding this comment.
Pull request overview
This PR adds AWS profile support for Bedrock credentials, enabling automatic credential refresh through the AWS SDK's credential provider chain. This enhancement allows users to configure AWS profiles with optional credential_process for seamless token rotation, eliminating manual updates of static environment variables when credentials expire.
Key Changes:
- Added
BEDROCK_AWS_PROFILEenvironment variable support for AWS profile-based authentication - Refactored credential loading logic to support three methods: user-provided credentials, static environment variables, and AWS SDK credential provider chain
- Added comprehensive documentation in
.env.examplewith configuration examples and AWS documentation references
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| api/server/services/Endpoints/bedrock/options.js | Refactored credential selection logic to support AWS profiles via credential provider chain, added logging for credential source transparency, and set profile property on llmConfig when applicable |
| .env.example | Added detailed documentation for AWS profile configuration including examples of credential_process setup and links to AWS documentation |
Comments suppressed due to low confidence (2)
.env.example:172
- Corrected spelling of 'support' to 'supported'.
# The following models are not support due to not supporting streaming:
.env.example:175
- Corrected spelling of 'support' to 'supported'.
# The following models are not support due to not supporting conversation history:
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.
That was already done - LibreChat-AI/librechat.ai#443 |
|
Hi @iElsha I can review/merge this soon. thanks for your patience. Also sent you a message on Discord so we can collaborate on better AWS compatibility |
|
@iElsha @danny-avila Any updates on the "add AWS profile support for Bedrock credentials" PR? |
|
Docs companion PR opened: LibreChat-AI/librechat.ai#584 This documents the Bedrock credential behavior added here, including |
|
@codex review |
|
Codex Review: Didn't find any major issues. Breezy! ℹ️ 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". |
…10504) - Add BEDROCK_AWS_PROFILE environment variable support - Implement AWS SDK credential provider chain for automatic refresh - Update credential loading logic to support profiles, static env vars, and user-provided credentials - Add logging for credential source transparency - Update .env.example with profile configuration documentation Follows S3 implementation pattern for credential handling. Enables users to configure AWS profiles with optional credential_process for automatic token refresh. Co-authored-by: Maxence - Meca.lu <contact@meca.lu> Co-authored-by: Danny Avila <danny@librechat.ai>
* 🪪 fix: Prevent MCP Server Name Collisions (#13256)
* fix: prevent MCP server name collisions
* chore: address MCP registry review nits
* fix: reserve MCP config names from request context
* chore: format MCP registry changes
* chore: address MCP collision review findings
* ⚖️ feat: Add Operational Prometheus Metrics (#13265)
* add operational prometheus metrics
* fix metrics review findings
* fix metrics edge cases
* gate mongoose metrics instrumentation
* gate metrics setup when unconfigured
* 🖲️ feat: Trace SSE Stream Lifecycle with OTel (#13266)
* ⌛ fix: Use JWT exp claim for MCP when OAuth token omits expires_in (#13248)
MCP OAuth access tokens are stored with a 365-day default expiry when the
provider's token response omits `expires_in` (only RECOMMENDED per RFC 6749
§5.1). Providers that issue short-lived JWT access tokens but omit
`expires_in` (e.g. Salesforce) therefore get tokens treated as valid for a
year and never refreshed, so every call 401s once the real token lapses
until the user manually reconnects.
When the access token is a JWT (RFC 9068), read its `exp` claim and use it as
the authoritative expiry, falling back to the 365-day default only for opaque
tokens. Explicit `expires_at`/`expires_in` still take precedence.
Adds unit tests for storeTokens expiry resolution.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* 🪪 fix: Support OpenID PKCE Without Client Secret (#12364)
* fix: allow OpenID PKCE authentication without client secret
* Linting
* Strategy fix
* fix(openid): trim secret gates and add PKCE client metadata tests
* chore(openid): normalize spec line endings
* ⚡ perf: Short-Circuit Config Override Resolution for Empty Principals (#12549)
Skip the getApplicableConfigs DB query when buildPrincipals returns
an empty array, since there are no principals to match against.
* ⚡ perf: Separate Error Handling for Principal Resolution vs Config Overrides (#12550)
Distinguish between buildPrincipals and getApplicableConfigs failures
so the uncached fallback to baseConfig is intentional and logged
separately from config override errors.
* Revert "⚡ perf: Separate Error Handling for Principal Resolution vs Config Overrides (#12550)"
This reverts commit 1729378a652f97e33ceded17c1b6735888670702.
* Revert "⚡ perf: Short-Circuit Config Override Resolution for Empty Principals (#12549)"
This reverts commit a100aa57382518e9525d0f98e7540ea5e8d09545.
---------
Co-authored-by: CMF\e-leite <EduardoLeite@criticalmanufacturing.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
* 📜 feat: Add Explicit new Skill Route from Agent Builder (#13119)
* fix: add explicit skills/new route
* style: fix test formatting
---------
Co-authored-by: parkjingyeong <sand1166@hyundai.com>
* 🪪 fix: Allow Optional client_secret for MCP OAuth (#12460)
* fix: Allow empty client_secret for MCP OAuth configuration
* fix: Enhance OAuth client registration logic to support predefined client_id and handle empty client_secret
* 🛂 fix: Detect OAuth Errors From HTTP 400 Responses (#11961)
* fix(mcp): detect non-standard OAuth errors from servers returning HTTP 400
* add tests for oauth error check
* fix(mcp): align factory OAuth error detection
* 🪬 fix: Skip MCP Tools When Required Custom User Vars Are Unset (#13152)
* fix: skip MCP tools when required customUserVars are unset (#10969)
* fix: whitespace-only values
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix: guard MCP registry lookup and unknown server config in customUserVars gate
* fix: fail closed on MCP registry lookup errors
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🆔 feat: Built-in Build Metadata for Support Triage (#12756)
* 🏗️ refactor: Derive App Version from Root package.json + Add buildInfo Schema
The hardcoded `Constants.VERSION` in `data-provider` is now replaced at
rollup build time via `@rollup/plugin-replace`, sourcing from the root
`package.json` so version bumps are a single-file change.
Adds the shape needed by the rest of the series:
- `interface.buildInfo` boolean flag (default `true`) — lets self-hosters
opt out of exposing commit/branch/date.
- `buildInfo` on `TStartupConfig` — commit/commitShort/branch/buildDate.
- `SettingsTabValues.ABOUT` — new settings tab enum value.
Ref: https://github.com/danny-avila/LibreChat/issues/12406
* 🛠️ feat: Add Build Metadata Resolver and Expose via /api/config
Adds `resolveBuildInfo()` in `@librechat/api` that surfaces commit SHA,
branch, and build date from (in order) `BUILD_*` env vars, then local git
metadata. Result is cached per-process.
`/api/config` includes a `buildInfo` field on both authenticated and
anonymous responses when `interface.buildInfo !== false` and at least one
resolver field is populated. Omitted entirely otherwise.
Designed so pre-built Docker images carry metadata via build-arg while
source installs pick it up from `.git` — no manual version tracking.
Ref: https://github.com/danny-avila/LibreChat/issues/12406
* ℹ️ feat: Add Settings → About Panel with Diagnostics Copy
New Settings tab that renders the running build's version, commit (short
SHA), branch, and build date in a monospaced block alongside a "Copy
diagnostics" button that emits a preformatted text blob for pasting into
support issues.
Tab is hidden when `interface.buildInfo` is set to `false`. Reads from
`startupConfig.buildInfo` provided by `/api/config`.
Ref: https://github.com/danny-avila/LibreChat/issues/12406
* 🐳 ci: Inject BUILD_COMMIT/BRANCH/DATE into Docker Images
Adds optional `BUILD_COMMIT`, `BUILD_BRANCH`, `BUILD_DATE` ARGs to both
`Dockerfile` and `Dockerfile.multi`, wired as `ENV` vars in the runtime
stage so the backend's `resolveBuildInfo` picks them up.
All image-publishing workflows (`tag`, `main`, `dev`, `dev-branch`,
`dev-staging`) now compute `${github.sha}`, `${github.ref_name}`, and a
UTC timestamp, then pass them to `docker/build-push-action` as
`build-args`.
Defaults are empty — non-CI builds (local `docker build`) still work,
and the backend falls back to local `.git` metadata if ARGs aren't set.
Ref: https://github.com/danny-avila/LibreChat/issues/12406
* 📝 docs: Direct Bug Reporters to Settings → About for Version Info
The previous instructions (`docker images | grep librechat`,
`git rev-parse HEAD`) only worked for a subset of deployments and
rarely produced a commit SHA for users pulling pre-built images.
Point users to the new in-app Settings → About panel's
"Copy diagnostics" button, which captures version, commit, branch,
build date, and user agent in a single preformatted block. Fallback
instructions preserved for older installs.
Ref: https://github.com/danny-avila/LibreChat/issues/12406
* 🐳 fix: Move BUILD_* ENV to End of Docker Stages to Preserve Layer Cache
Per-commit BUILD_COMMIT/BUILD_DATE changes were being promoted to ENV
before `npm ci` / `npm run frontend` (single-stage) and before
`npm ci --omit=dev` (multi-stage api-build), which invalidated the cache
for every subsequent layer on every CI run.
Move the ARG/ENV block below the heavy install and build steps in both
Dockerfiles. Metadata is still available in the runtime image but no
longer busts layer reuse.
Addresses codex review on #12756.
* 🔧 fix: Propagate interface.buildInfo=false to Unauthenticated /api/config
The unauthenticated branch of `/api/config` was emitting an `interface`
object only when `privacyPolicy` or `termsOfService` was set, which
meant an admin's explicit `interface.buildInfo: false` opt-out was never
visible to anonymous/guest clients. `Settings.tsx` gates the About tab
on `startupConfig?.interface?.buildInfo !== false`, so a missing field
fell through as "enabled" for those clients.
Include `interface.buildInfo: false` in the unauth payload whenever it's
explicitly disabled. Keep the implicit default (true) absent to preserve
the minimal-unauth-payload convention.
Addresses codex review on #12756.
* 🔀 ci: Trigger Dev Image Workflows on Root package.json + Dockerfile Changes
The baked `Constants.VERSION` now reads from the root `package.json` via
rollup-plugin-replace, but the `dev-images.yml` and `dev-branch-images.yml`
path filters only matched `api/**`, `client/**`, `packages/**`. A release
commit that only bumps root `package.json` would not trigger a rebuild,
leaving `latest` dev images with stale Footer/About version metadata.
Include `package.json`, `package-lock.json`, and both Dockerfiles in the
path filters so dependency changes (lockfile rebuilds) and image build
tweaks also rebuild dev images.
Addresses codex review on #12756.
* 🧽 fix: Harden About Panel Lifecycle, A11y, and Loading Gate
Review follow-ups on #12756:
- #1 timer leak: stash the copy-state `setTimeout` in a ref and clear it
from a `useEffect` cleanup so unmounting the Settings dialog mid-toast
doesn't fire `setCopied(false)` on an unmounted component.
- #3 flash of About tab: gate `aboutEnabled` on `startupConfig != null`
so the tab stays hidden until `/api/config` returns. For admins who
disabled `interface.buildInfo`, the tab no longer briefly appears and
vanishes on page load.
- #6 aria-live placement: move the live region off the interactive
button onto a dedicated `<span role="status" aria-live="polite">` so
screen readers announce the copied state, not the full button content
on every re-render.
- #2 missing coverage: add `About.spec.tsx` exercising populated/empty
buildInfo rendering, invalid-date handling, diagnostics clipboard
payload, copy-state toggling, unmount cleanup, and the live region.
* ⚡ perf: Eagerly Resolve Build Info at Module Load
Review follow-up #4 on #12756: `resolveBuildInfo()` calls `execFileSync`
with a 2s timeout on source installs without `BUILD_*` env vars. Paying
this cost on the first HTTP request blocks the event loop mid-flight.
Call `resolveBuildInfo()` once at config route module load so the
resolver's cache is warm before any request arrives. Docker images with
the BUILD_* env vars set sidestep the git path entirely, so this only
affects the edge case of source installs.
* 📝 docs: Document rollup Version Placeholder Contract
Review follow-ups #5 / #8 on #12756. The `__LIBRECHAT_VERSION__`
placeholder relies on a substring replacement rule that only works
because the token appears inside a string literal, and the substitution
only runs during `npm run build:data-provider`.
- Expand the `Constants.VERSION` JSDoc to spell out that consumers read
the placeholder through the built dist bundle; source-level test
imports would see the raw placeholder.
- Add a NOTE above the rollup `replace` config warning future
contributors not to repurpose the token as a bare identifier without
switching to a quoted replacement value.
Non-functional; prevents future contributors from stepping on a subtle
constraint.
* 🪪 fix: Only Toast "Copied" When Clipboard Copy Actually Succeeds
Codex R5 on #12756. `copy-to-clipboard` returns a boolean indicating
whether the underlying `execCommand('copy')` / fallback prompt actually
wrote to the clipboard. The previous handler flipped to the "Copied"
state unconditionally, which in hardened browsers or when the
permission prompt is dismissed would mislead users into filing bug
reports without the diagnostics blob attached.
Gate the state/timer/live-region on the boolean return; silently no-op
on failure rather than showing a false positive. Adds a test asserting
the button label stays at "Copy diagnostics" when the clipboard call
fails.
* 🐳 fix: Derive main image metadata from checkout
* 🪪 fix: Keep About enabled until disabled
* ✅ test: Avoid literal Settings mock text
* 🧱 refactor: Rename Build Info Module
* 🪨 fix: Normalize Empty MCP Tool Descriptions to `undefined` for Bedrock Compat. (#13217)
* fix(agents): normalize empty MCP tool descriptions to undefined
MCP servers (e.g. Asana MCP) can return tools with an empty string
description. AWS Bedrock's converse API rejects toolSpec.description
with length < 1, so any empty-description MCP tool caused the entire
request to fail with a validation error.
Convert empty strings to undefined at the two sites in
loadToolDefinitions where MCP tool definitions are built. An undefined
description is omitted from JSON serialization, so Bedrock never sees
the empty value. OpenAI and Anthropic direct APIs are unaffected.
Fixes #13209
* test(agents): add coverage for empty MCP tool description normalization
Verify that loadToolDefinitions converts '' descriptions to undefined
for both the sys__all__sys pattern and directly named MCP tools.
* 🛂 feat: Add AWS Profile Support for Bedrock Credentials (#10504)
- Add BEDROCK_AWS_PROFILE environment variable support
- Implement AWS SDK credential provider chain for automatic refresh
- Update credential loading logic to support profiles, static env vars, and user-provided credentials
- Add logging for credential source transparency
- Update .env.example with profile configuration documentation
Follows S3 implementation pattern for credential handling.
Enables users to configure AWS profiles with optional credential_process for automatic token refresh.
Co-authored-by: Maxence - Meca.lu <contact@meca.lu>
Co-authored-by: Danny Avila <danny@librechat.ai>
* 📡 fix: Tighten Streaming Message Cache Preservation (#13271)
* fix: tighten streaming message cache preservation
* fix: sync streaming ref after commit
* 🪙 feat: Add AWS Bedrock API key support (#8690)
* feat: Add Bedrock API key support
* fix: Respect Bedrock credential mode
* fix: Support mixed Bedrock credential forms
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🧷 fix: Harden MCP Proxy SSRF Checks (#13274)
* 🧱 fix: Validate Bedrock User Credentials (#13277)
* 🧷 fix: Pin MCP OAuth Client Secrets (#13276)
* fix: Pin MCP OAuth client secrets
* fix: Require MCP OAuth client id for secrets
* 🏣 fix: Reject System Tenant In Auth Context (#13278)
* 🛰️ feat: Support Gemini Tool Combinations (#13273)
* feat: support Gemini tool combinations
* test: align Gemini provider tool shape
* fix: avoid duplicate native web search tools
* fix: gate Gemini tool combinations by model
* 🧾 fix: Validate Bedrock User Credentials (#13279)
* 🚦 ci: Enforce ESLint on Package Changes (#13280)
* 🪟 fix: Apply Admin-Panel Config Overrides To YAML-Defined MCP Servers (#13173)
* 🪟 fix: Apply Admin-Panel Config Overrides To YAML-Defined MCP Servers
Admin-panel saves of MCP server fields for YAML-defined servers were
silently dropped by the registry. ensureConfigServers filtered out any
merged config entry whose name appeared in YAML, so overrides such as
iconPath, title, and description never reached getAllServerConfigs even
though the override row had been written to the configs collection and
the AppConfig merge layer had produced the correct merged result.
The filter is removed and replaced with a content-equivalence
short-circuit in ensureSingleConfigServer. YAML-defined servers whose
merged config matches the YAML cache entry skip lazy-init, so unmodified
YAML servers still avoid a redundant inspection round trip. The new
private helper matchesYamlConfig reuses the existing content-hash
function on configurable fields only.
getAllServerConfigs now overlays config-tier entries onto the YAML base
while preserving user-DB entries (source: 'user'), giving precedence of
YAML, then Config tier, then User DB. The docstring is updated to
describe the new order.
Multi-tenancy is already enforced upstream of the registry by the
AppConfig layer, so the registry stays tenant-agnostic and overrides
remain isolated per tenant.
Tests cover the new behavior: config-tier override on YAML-defined
server flows through to getAllServerConfigs, YAML servers without
effective overrides skip lazy-init, user-DB entries win over
config-tier overlays, pure config-tier servers still lazy-init, and the
merged config passed to lazy-init preserves all YAML fields when the
override only adds new ones.
* 🛠️ fix: Address Review Feedback For YAML Override Precedence
Both Copilot and Codex flagged matchesYamlConfig as broken in
production: the cached YAML config carries inspector-derived defaults
(requiresOAuth defaulted to false when YAML omits it, serverInstructions
rewritten from the YAML toggle to the fetched server-instructions
string, and so on) that are absent from appConfig.mcpConfig. The
content-hash comparison reports a mismatch for every YAML server with
no admin-panel override, so ensureSingleConfigServer re-inspects all of
them anyway. The optimization never fires in practice and reintroduces
the source tagging it was supposed to avoid.
Remove the short-circuit and the matchesYamlConfig helper. YAML-defined
servers that appear in the merged config go through lazy-init like any
other entry. A smarter optimization that overlays cosmetic-only
override fields onto the YAML cache without re-inspection belongs in a
follow-up once the boundary between configurable fields and
inspector-derived fields is well defined.
Update the existing ensureConfigServers tests that asserted the old
filter behavior (should exclude YAML servers from config-source
detection, should return empty when all servers are YAML) to assert
the new behavior: YAML servers pass through ensureConfigServers and
are lazy-initialized when they appear in the merged config. Make the
inspector mock more realistic by spreading the raw input first and
overlaying only runtime fields, so the test fixtures match production
where the inspector preserves configurable fields. Drop the companion
test in MCPServersRegistry.test.ts that asserted the short-circuit
fires for unchanged YAML servers; the hand-crafted fixture skipped
inspector defaults and was not representative.
Copilot also flagged that getServerConfig short-circuits to
configServers before checking the user DB, so the precedence enforced
in getAllServerConfigs (user-DB beats config-tier) was bypassed in the
single-server lookup. When configServers carries an entry and a userId
is available, check the user DB first and prefer a source: 'user'
entry so per-user servers are never shadowed by an admin-panel
override.
* 🛡️ fix: Harden Admin Override Overlay Against Failure Stubs
Hardens the admin-panel override path against transient inspect failures
and removes a defensive branch that guarded an impossible state.
The getAllServerConfigs overlay now skips failed-inspection stubs so a
healthy YAML or DB entry stays visible during the 5-minute retry window
instead of being clobbered by a stub. When the overlay does land, the
base entry's source tier is preserved, which keeps Tools/mcp.js routing
its failed-inspection recovery to the correct storage location.
The user-DB precedence block in getServerConfig is removed: configServers
is built from appConfig.mcpConfig which only ever carries admin-tier
entries, so the DB lookup defended a state that cannot occur via the
current call graph. The dead yamlServerNames memoization is also gone.
Adds two regression tests covering inspection-failure preservation and
source-tier preservation on successful overlay, and adds a debug log
when an admin override is suppressed by a user-tier entry. The
makeParsedConfig test factory now honors overrides correctly.
* 🧪 test: Strengthen Admin Override Coverage And Docs
Adds an end-to-end regression test that chains MCPServerInspector.inspect
failure through ensureConfigServers and getAllServerConfigs, asserting
the healthy YAML base entry survives a transient inspect failure
intact. The previous regression test hand-built a failure stub and
skipped ensureSingleConfigServer, leaving the production chain itself
untested.
The getAllServerConfigs docstring now spells out both overlay guards
(failed-stub skip and user-tier preservation) and the source-field
preservation contract that downstream recovery logic depends on.
The yamlLangfuseConfig test fixture is frozen so a future test cannot
mutate it and contaminate sibling tests in the describe block.
* 🔧 fix: Skip Lazy-Init For Unchanged YAML MCP Servers
Adds an admin-configurable-field equivalence check so YAML-defined MCP
servers that carry no admin override skip lazy-init in
ensureConfigServers. This avoids the per-request inspect storm and
keeps unmodified YAML servers out of the config-tier cache, so admin
saves that touch unrelated overrides no longer evict and tear down
those YAML connections.
A second guard in getServerConfig prevents failed-inspection stubs in
configServers from shadowing the healthy YAML base entry for the
duration of the retry window. The aggregate path already had this
guard via getAllServerConfigs; this brings the single-server path to
parity, so Tools/mcp.js recovery routes to YAML reinspection rather
than bailing on the config retry timer.
Adds three regression tests covering the unmodified-YAML skip, the
admin-override lazy-init trigger, and the failure-stub fallthrough.
Updates two existing ensureConfigServers tests that previously
documented the now-incorrect "always lazy-init YAML" behavior.
* 🔓 feat: Expose baseOnly Flag On Admin Config Base Endpoint
The admin getBaseConfig handler now reads req.query.baseOnly and
forwards it to getAppConfig so an admin panel client can request the
un-merged YAML and AppService base configuration without DB overrides
applied. The flag is opt-in; existing callers see no behaviour change
because the default remains the merged response.
The query value is coerced through String() so Express array forms
like baseOnly=true&baseOnly=true are treated as false rather than
truthy by accident. A handler test pins the forwarding behaviour and
the default-merged behaviour against future regressions.
* 🧹 fix: Address Codex Review Findings On MCP Registry Precedence Path
Four follow-ups from the Codex review of PR #13173:
P1. getServerConfig now preserves the configServers candidate as a
last-resort fallback when both YAML cache and user DB return nothing,
so admin-defined config-only servers carrying inspectionFailed=true
still surface the failure stub to callers in api/server/services/Tools/mcp.js
that rely on it to return the still-unreachable message. The
not-found memoization is preserved.
P2a. proxy is added to ADMIN_CONFIGURABLE_FIELDS so an admin override
on SSE/streamable-http proxy is no longer treated as an unchanged YAML
server and correctly triggers lazy-init.
P2b. isUnmodifiedYamlServer now treats absent-on-rawConfig fields as
equal, so inspector-derived values on the cached YAML entry
(notably requiresOAuth filled in by detectOAuth at startup) do not
force unmodified YAML servers to re-init on every request.
P3. getBaseConfig parses ?baseOnly strictly against the literal string
true instead of String-coercing, so array shapes like baseOnly[]=true
no longer pass through.
Regression tests cover all four paths.
* 🧹 fix: Drop Misleading Shadow Warning On Config Vs User-DB Collisions
The Config-tier branch of warnOnOperatorManagedNameCollisions logged
that Config MCP servers shadow DB-backed servers, but
getAllServerConfigs actually preserves the user-tier entry on a
Config-vs-user collision and skips the override. The warning was
describing the opposite of what the code does and would mislead
operational debugging.
The YAML-tier call is unchanged because YAML still legitimately
shadows DB-backed servers. The per-entry debug log inside the
collision branch already captures the actual outcome.
Test renamed and rewritten to assert the user-tier entry is
preserved and no shadow warning is emitted.
* 🔒 fix: Keep Tenant-Scoped configServers Candidate Out Of The Global Read-Through Cache
The prior fix for surfacing inspectionFailed stubs from admin-defined
config-only servers wrote the per-call configServers candidate into
readThroughCache when YAML and DB both missed. The cache key is keyed
by serverName plus userId, so a failed stub from one tenant could
satisfy a later no-userId lookup made by another tenant before any
configServers resolution ran.
getServerConfig now caches only the global YAML/DB resolution (still
caching undefined to memoize not-found lookups) and uses the candidate
strictly as an unmemoized function-level fallback that surfaces the
failure stub to the caller without leaking it across tenants.
Regression test exercises a no-userId call after a tenant-scoped
failure and asserts the cache returns undefined rather than the
stub, and that a second tenant sees their own healthy candidate.
* 🔄 fix: Mirror getAllServerConfigs Precedence Exactly In getServerConfig
getServerConfig was short-circuiting with the configServers candidate
on every healthy lookup, which made single-server callers diverge from
the aggregate path for name collisions between config-tier overrides
and user-DB entries. The aggregate path preserves the user-tier entry
on such collisions, so single-server callers saw the admin override
while list views saw the user server for the same name.
getServerConfig now resolves the YAML/DB base first and applies the
same four-step precedence used in getAllServerConfigs:
1. user-tier base wins absolutely over a config-tier candidate
2. healthy YAML/DB base wins over a failed (inspectionFailed)
candidate
3. healthy candidate overlays its fields onto the base, preserving
the base entry's source tag so downstream recovery routes to the
correct storage location
4. with no base, the candidate is returned as-is for config-only
servers
readThroughCache still memoizes only the global YAML/DB lookup, so
the per-call configServers candidate never enters the cache and the
tenant-isolation guarantee from the previous fix is preserved.
Regression tests cover the user-wins-over-config case and the
YAML-overlay-with-yaml-source-preserved case.
* ⚡ perf: Batch YAML Cache Read In ensureConfigServers
isUnmodifiedYamlServer was calling cacheConfigsRepo.get(serverName)
per entry. In the Redis aggregate-key backend, get() is implemented
as getAll() then map lookup, so N concurrent per-server lookups
inflate into N full-map reads and deserializations on every
ensureConfigServers pass.
The loop now takes a single getAll() snapshot at the top and hands
it into a synchronous isUnmodifiedYamlServer helper, turning O(n)
remote reads into O(1) regardless of how many MCP entries are
resolved. The snapshot also gives the unchanged-YAML comparison
one consistent view of YAML across all entries.
Regression test spies on cacheConfigsRepo.get and asserts it is
never called from ensureConfigServers, with getAll called exactly
once.
* 🎨 chore: prettier --write all workspaces (#13281)
Run `prettier --write` over the source trees of every workspace to align
with the repo's own `.prettierrc` (`printWidth: 100`, `singleQuote: true`,
`trailingComma: 'all'`, etc.). **19 files reformatted total** — purely
whitespace and line-wrap changes, no functional edits and no API changes.
Scope:
- `packages/api/src/**/*.{ts,tsx}` — 14 files
- `packages/client/src/**/*.{ts,tsx}` — 1 file
- `packages/data-schemas/src/**/*.{ts,tsx}` — 4 files
- `api/**`, `client/**`, `packages/data-provider/**` — already prettier-clean
Most of the drift is in argument-list / type-annotation wrapping where
the formatted form fits within `printWidth` but the current source keeps
a hand-wrapped multi-line shape. Example:
// before
function countWebSearchDefinitions(
toolDefinitions: Array<{ name: string }> | undefined,
): number { … }
// after (still well under 100 cols)
function countWebSearchDefinitions(toolDefinitions: Array<{ name: string }> | undefined): number { … }
`npx prettier --check` across all workspaces is now clean. The local
pre-commit hook (`lint-staged` → `prettier --write`) would have produced
the same result on any future edit to these files.
There are no prettier-checking workflows in CI today, so drift like this
can re-appear if PRs are merged with the hook bypassed. Companion PR
#13282 adds a `prettier --check` step to `eslint-ci.yml` so future
drift gets caught.
* 🎨 ci: Check Prettier Formatting Drift on Package Changes (#13282)
Adds a `Run Prettier --check on changed files` step to the existing
`eslint-ci.yml` workflow. Same path filter (api/**, client/**,
packages/**), same changed-files detection, runs after the ESLint step.
## Why
Today there is no `prettier --check` in CI — only the local
`lint-staged` pre-commit hook runs `prettier --write`. When a PR is
merged with the hook bypassed (e.g. GitHub UI edit-and-merge, or
`git commit --no-verify`), a file can land in a non-prettier-canonical
state and nobody notices. The next contributor who stages an unrelated
change in that file then ends up with a "drive-by" prettier diff in
their PR.
`packages/api/src` had 14 such files accumulated; #13281 fixes the
existing drift. This PR closes the gap so it doesn't regrow.
## What the step does
- Detects changed JS/TS files under `api/**`, `client/**`, or
`packages/**` against the PR base.
- Runs `npx prettier --check $CHANGED_FILES`.
- On failure, prints a `::error::` annotation telling the contributor
how to fix it locally (`npx prettier --write <files>`).
Same one-step shape as the existing ESLint check — no extra workflow
file, no extra `npm ci`, no extra checkout.
## Ordering note
#13281 (`chore: prettier --write packages/api/src`) should land first
so the existing drift is cleared. After both PRs merge, the
pre-commit hook + this CI check together prevent drift from
re-accumulating.
## Test plan
- [x] `npx js-yaml .github/workflows/eslint-ci.yml` validates.
- [ ] CI green on this PR itself (touches only `.github/workflows/`,
which the path filter includes, so the workflow runs on itself).
- [ ] After merge: a synthetic PR introducing prettier drift should
fail the new step with the diagnostic message.
* 🪤 chore: Prevent CI Path Argument Injection (#13284)
* 🌐 chore: Guard Locize Reviewer Request (#13286)
* 🌍 i18n: Update translation.json with latest translations (#13283)
* 🪡 fix: Prevent Hover Actions Flash While Streaming (#13285)
* 🌍 i18n: Update translation.json with latest translations (#13291)
* 🧩 fix: Add REDIS_CLUSTER_SAFE_DELETE Flag for ElastiCache Serverless CROSSSLOT Errors (#13275)
* fix(redis): add REDIS_CLUSTER_SAFE_DELETE for ElastiCache Serverless CROSSSLOT errors
ElastiCache Serverless and similar managed Redis services present a single-node
connection endpoint but shard keys internally. When USE_REDIS_CLUSTER=false (as
required for single-endpoint services), batchDeleteKeys() uses multi-key DEL
commands that fail with CROSSSLOT errors because the managed cluster rejects
cross-slot operations.
Adds REDIS_CLUSTER_SAFE_DELETE=true which forces per-key deletion (the same
cluster-safe path) without changing the connection mode. This makes the delete
strategy independent of the connection topology.
Closes #13261
* test(cache): add REDIS_CLUSTER_SAFE_DELETE config tests
* fix: Avoid nested Redis delete mode ternary
* docs: Add Redis cluster-safe delete env example
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🧬 refactor: Derive Latest Message From Cache (#13294)
* fix: derive latest message from cache
* fix: clear changed-file lint warnings
* fix: address branch tail review feedback
* fix: keep latest message on new chat stream key
* 🧵 fix: Prevent Message Loading Race During Streaming (#13295)
* ⏭️ fix: Avoid False Resume Submission Stale Detection (#13297)
* fix: avoid false stale resume submission detection
* style: format resume on load spec
* test: cover delayed resume message identity
* ⏱️ refactor: Optimistically Show New Chats In Sidebar (#13298)
* fix: optimistically show new chats in sidebar
* fix: reconcile optimistic conversation ids
* fix: clean optimistic conversation edge cases
* 📦 chore: bump `@librechat/agents`, `qs`, `langfuse` (#13299)
* chore: Update @librechat/agents to version 3.1.93 and @langfuse packages to version 5.3.0 in package-lock.json and package.json files
* chore: Update browserify-sign to version 4.2.6 and qs to version 6.15.2 in package-lock.json
* 🧯 fix: Suppress Google Service Key Noise (#13322)
* fix: suppress unused Google service key noise
* test: stabilize Google endpoint config mocks
* fix: normalize Google service key endpoint config
* 📦 chore: bump `@librechat/agents` to v3.1.94 (#13306)
* 🪢 fix: Coerce Tool Execution Args By Schema (#13310)
* fix: Coerce tool execution args by schema
* 📦 chore: bump `@librechat/agents` to v3.1.95
* 🖱️ fix: Add MCP Hover Action Background (#13319)
* 🌍 i18n: Update translation.json with latest translations (#13325)
* ✂️ fix: Truncate Long MCP Server Titles In Builder Panel (#13321)
In SidePanel/MCPBuilder/MCPServerCard the server title was rendered in
a span carrying the Tailwind truncate utility. overflow: hidden does
not apply to inline boxes, so long server names overflowed their slot
and ran underneath the Edit, Reconnect, and Revoke icons.
The title now renders in a div so truncate actually clips with an
ellipsis. No other styling changes.
* 📦 chore: bump `@librechat/agents` to v3.1.96
* 🐢 ci: Raise test-packages-api timeout to 20 min (#13326)
The `Tests: @librechat/api` job in `backend-review.yml` had
`timeout-minutes: 10`. Recent runs have started getting cancelled
right at the 10-minute mark with tests still actively passing — the
log streams `PASS …` lines through the final second before
`The operation was canceled.` fires.
Looking at recent runs in the wild:
- Warm `ubuntu-latest` runners: suite finishes in ~4–5 min.
- Cold/busy runners: 10+ min. No headroom = job killed mid-suite.
The suite has grown over the last few months (OpenTelemetry, MCP
OAuth, Bedrock, the Operational Prometheus Metrics work from #13265,
plus the agents/responses harness). 10-minute ceiling that worked
before is now a tail-latency cliff.
Bump to 20 minutes. Still well below the runner's hard 6-hour cap,
generous enough to absorb runner variance, and a stuck/genuine hang
will still get killed eventually rather than billing forever.
No code changes; one-line config bump.
* 📦 chore: bump `@librechat/agents` to v3.1.97
* 📎 fix: Preserve Gemini PDF Media Blocks (#13357)
* 🛡️ fix: Harden Model Spec Icon Rendering (#13356)
* fix: Harden model spec icon rendering
* docs: Clarify model spec icon fallback
* 🪡 fix: Artifact Edit Saves (#13358)
* fix artifact edit saves
* style format artifact updater
* fix artifact save review findings
* move artifact updater to api package
* fix artifact updater type check
* fix artifact edit review findings
* style format artifact editor guard
* fix artifact parser fallback scope
* fix artifact fallback overwrite
* 🧠 feat: Add Claude Opus 4.8 Support (#13380)
* feat: add Claude Opus 4.8 support
* fix: omit sampling params for Claude Opus 4.8
* fix: flatten Bedrock beta header merge
* fix: strip Bedrock sampling params for Opus 4.8
* 📋 fix: Cap Default Limit on Agent List Queries (#13382)
* 🛡️ fix: Cap Default Limit on Agent List Queries (#13363)
`GET /api/agents` accepted unbounded requests: when the client omitted
`limit`, the value flowed straight into `getListAgentsByAccess`, which
set `isPaginated = false` and issued an uncapped MongoDB query. Combined
with the unindexed `findPubliclyAccessibleResources` AclEntry scan run
on every request, this produced 10-19s response times and stalled the
connection pool on instances with 100+ agents.
- Default `limit` to 100 in the route handler so client requests without
`?limit=` paginate by default.
- Default `limit` to 100 in `getListAgentsByAccess` itself as
defense-in-depth. The function already caps numeric limits at 100, so
there is no client-facing change.
- Pass `limit: null` explicitly in the actions route, which legitimately
needs the full editable-agent set, to preserve its existing behavior.
- Add regression tests covering the default cap and the explicit
unbounded opt-out.
* 🛡️ fix: Avoid agent-list regression for users with 100+ agents
Codex review pointed out that capping `getListAgentsByAccess` at 100
silently truncated agents past the first page for the four consumers
(`useAgentsMap`, `AgentSelect`, `ModelSelectorContext`, `useMentions`)
that read `res.data` without following `has_more`/`after`.
- Raise the function's hard cap from 100 to 1000 to match
`MAX_AVATAR_REFRESH_AGENTS`, the realistic upper bound the
avatar-refresh path already assumes. (Side effect: the avatar refresh
call site was silently being capped at 100 by the old normalize step.)
- In `useListAgentsQuery`, merge `limit: 1000` into params so the four
consumers above get the user's full accessible set in a single
round-trip instead of needing cursor pagination.
- Route handler default stays at 100 as defense-in-depth for any other
caller that omits `limit`.
- Add a regression test asserting an explicit `limit` above 100 now
returns the full set instead of being clipped.
* 🪢 fix: Keep agent-list cache key stable for mutations
Codex P2 review noted that folding `limit: 1000` into the cache key
broke `allAgentViewAndEditQueryKeys` in `Agents/mutations.ts`, which
references `[QueryKeys.agents, { requiredPermission }]` directly across
eight mutation handlers. After my prior change the cached entry lived
under `[QueryKeys.agents, { limit: 1000, requiredPermission }]`, so
create/update/delete/avatar/action mutations stopped updating the list
the four consumer hooks render — and with `refetchOnMount` and focus/
reconnect refetches disabled, the UI would stay stale until something
else triggered a fetch.
Split the merged limit out of the cache key: the request to
`dataService.listAgents` still uses `requestParams` (with the default
limit applied), but the React Query cache key uses the caller's `params`
as-is. The mutation cache updates land again, and the request still
returns the user's full accessible set in one round-trip.
* 🛡️ fix: Index AclEntry and paginate agent list internally (#13363)
Completes the perf fix for #13363 properly — resolves both the
unbounded ACL scans Copilot flagged and Codex's tension between "show
all agents" and "don't bypass the server cap".
Backend:
- Add a compound index on `{ principalType, resourceType, permBits,
resourceId }` to the AclEntry schema. This is the index missing for
`findPublicResourceIds` and the public branch of the `$or` in
`findAccessibleResources`, both of which previously fell back to a
collection scan on every `GET /api/agents`. Adds an `explain`-based
regression test asserting the public query no longer COLLSCANs.
Client:
- Rewrite `useListAgentsQuery` to follow the server's cursor
pagination internally and concatenate every page into a single flat
`AgentListResponse`. Consumers (`useAgentsMap`, `AgentSelect`,
`ModelSelectorContext`, `useMentions`) get the user's complete
accessible-agent set without any of them needing to learn about
cursors, and each individual request uses the server's default
page size (so the route's 100-default defense-in-depth fires for
real). Cache key shape is unchanged, so the eight mutation handlers
in `Agents/mutations.ts` keep matching `allAgentViewAndEditQueryKeys`
and update the cached list as before.
- Drop the `FULL_AGENT_LIST_LIMIT = 1000` injection added in the
previous commit — no longer needed once pagination handles the full
set, and removing it stops bypassing the route default.
* 🧹 fix: CI fallout from C-done-properly refactor
- Collapse multi-line `fetchAllAgentPages` signature in queries.ts so
prettier stops complaining.
- In the new public-principal index test, grant one ACL entry before
calling `.explain()` so the collection exists (otherwise mongo returns
`nonExistentNamespace` and there is no winning plan to inspect).
- Cast the `.explain('queryPlanner')` result to a typed shape — the
mongoose return type doesn't expose `queryPlanner` directly and was
failing the TypeScript check.
* 🧪 fix: Test the AclEntry public-principal index via hint, not planner choice
The previous test asserted the query planner did not pick COLLSCAN for
the public-principal lookup. That assertion fails on small collections
(under the planner's collection-size heuristic) — the index exists and
is usable, but with a single document in the test the planner correctly
chooses COLLSCAN as the cheaper plan.
Reshape the assertion:
1. Confirm the new compound index is actually declared by inspecting
`collection.indexes()` after `syncIndexes()`.
2. Force the planner to that index via `.hint()` and assert the winning
plan is `IXSCAN` — proves the index is real and serves this query
shape, without depending on collection-size heuristics.
* 🧹 chore: Slim down verbose comments
The JSDoc and inline comments added across the perf fix had drifted
into multi-paragraph rationale better suited to the PR description than
the source. Collapse to single-line JSDoc that just describes what each
piece does; drop the inline comment in `actions.js` entirely — the call
is self-evident.
* 🪨 fix: Preserve Bedrock Guardrail Config (#13381)
* 🧠 refactor: Replay DeepSeek `reasoning_content` via OpenRouter (#13368)
* 🧠 fix: Replay DeepSeek `reasoning_content` via OpenRouter
DeepSeek's thinking-mode API rejects multi-turn tool-calling requests
unless `reasoning_content` from each tool-bearing assistant message is
replayed verbatim, returning HTTP 400 "The `reasoning_content` in the
thinking mode must be passed back to the API." The agents SDK already
handles this for direct `Providers.DEEPSEEK`, but DeepSeek models routed
via OpenRouter use `Providers.OPENROUTER` — `formatAgentMessages` skipped
the reasoning-preservation branch, and `ChatOpenRouter` left
`includeReasoningContent` unset, so the field silently dropped on every
subsequent turn.
Add `isDeepSeekReasoningProvider(provider, model)` and use it in two
places: (1) `getOpenAILLMConfig` flips `includeReasoningContent: true`
when OpenRouter is dispatching a `deepseek/*` model so the LangChain
client emits the field on assistant turns that have non-empty
`additional_kwargs.reasoning_content`, and (2) `AgentClient` spoofs the
provider hint to `Providers.DEEPSEEK` when calling
`formatAgentMessages`, triggering the SDK's existing
`preserveReasoningContent` path that re-attaches the field to
reconstructed tool-bearing AIMessages. The downstream
`_convertMessagesToOpenAIParams` is already gated on non-empty
`reasoning_content`, so the flag is a no-op outside thinking mode.
Resolves #13366.
* fix: Harden DeepSeek detection against OpenRouter routing edges
Address three Codex review findings on #13368:
1. Strip OpenRouter's `~` latest-routing prefix before applying the
DeepSeek model regex. `~deepseek-chat` and `~deepseek/r1` were
previously left unmatched because the regex's start/`/` boundary
only saw the `~`. Mirror the SDK's `normalizeOpenRouterModel()`
here and in `getOpenAILLMConfig`.
2. Add a custom-endpoint fallback: when the model id carries the
unambiguous `deepseek/...` OpenRouter namespace, accept it
regardless of the resolved provider. Covers the case where a user
configures OpenRouter under a non-standard endpoint name and
`initializeAgent` normalizes the unknown provider to `openai`,
stranding the spoof. Bare `deepseek-*` ids still require an
explicit DeepSeek/OpenRouter provider so unrelated endpoints
labelling a model `deepseek-r1` don't trigger.
3. Inspect every agent in `this.agentConfigs` when deciding whether
to spoof the format provider. Multi-agent handoff runs feed all
agents' messages through one `formatAgentMessages` call, so a
DeepSeek handoff under a non-DeepSeek primary previously lost its
persisted reasoning_content too.
Also addresses Copilot's review note: only pass the options object
to `formatAgentMessages` when the DeepSeek spoof is actually needed,
preserving the pre-fix behavior for everyone else.
* fix: Extend DeepSeek reasoning_content fix to OpenAI-compat agent paths
Address two more Codex P2 findings on #13368:
1. `getOpenAILLMConfig` no longer gates `includeReasoningContent` on
`useOpenRouter`. Any DeepSeek-style model id (with `~` latest-routing
prefix stripped) is sufficient. This re-aligns the LLM gate with
`AgentClient`'s formatter spoof, which already treats a `deepseek/*`
id as authoritative — so a custom-named OpenRouter endpoint or a
DeepSeek-compatible proxy gets the field both attached to history AND
serialized to the wire. Direct `ChatDeepSeek` ignores the flag (its
own conversion path hardcodes `includeReasoningContent: true`), so
this is a harmless no-op there.
2. Thread the same `Providers.DEEPSEEK` formatter hint through
`api/server/controllers/agents/openai.js` and `responses.js` (the
OpenAI-/Responses-compatible serving paths). Without it those paths
restored `additional_kwargs.reasoning_content` only in `AgentClient`
while the LLM config flipped `includeReasoningContent` on for them
too — so DeepSeek tool turns served from those endpoints would still
ship requests with the flag set but no field present, hitting the
same second-turn 400. The `needsDeepSeekFormatHint` helper in
`openai.js` mirrors `AgentClient`'s per-agent check.
* fix: Tighten DeepSeek detection and cover handoff sub-agents
Address four more Codex P2 findings on #13368:
- Tighten the DeepSeek model regex to `^deepseek(?:[-/]|$)/i` (anchored
to start). Rejects cloned/distilled slugs like
`mistral/deepseek-distilled-foo` and `community/deepseek-r1` that
previously matched via the `(?:^|/)` alternation, which could attach
the DeepSeek-only `reasoning_content` field on proxies that don't
accept it.
- Anchoring also collapses the namespace-only fallback into the same
pattern, so bare `deepseek-chat` / `deepseek-reasoner` on a
custom OpenAI-compatible DeepSeek proxy are now recognized — fixing
the asymmetry where `getOpenAILLMConfig` would flip
`includeReasoningContent` for those bare ids but `AgentClient`
wouldn't pass the formatter hint.
- Extend `needsDeepSeekFormatHint` in `openai.js` (and the inline
check in `responses.js`) to walk `handoffAgentConfigs` too. In
multi-agent runs where the primary isn't DeepSeek but a connected
handoff agent is, the SDK's `formatAgentMessages` previously dropped
the handoff's persisted reasoning_content before the next tool turn,
preserving the 400 the PR was meant to prevent.
- Mirror the regex change in `getOpenAILLMConfig`.
Out of scope: the OpenAI-compatible serving paths still don't
preserve incoming `reasoning_content`/`reasoning` fields in
`convertMessages`, nor does the Responses API persist reasoning in
`saveResponseOutput`. Those are deeper persistence/conversion fixes
worth a separate PR.
* test: Allow includeReasoningContent for Azure-serverless DeepSeek
CI surfaced a backward-compat expectation that snapshotted the
pre-fix behavior. Azure-serverless DeepSeek deployments (e.g.
`DeepSeek-R1`) forward to the same DeepSeek thinking-mode tool-call
contract, so the LLM gate now correctly flips
`includeReasoningContent: true` for them too. The downstream
gate on a non-empty `additional_kwargs.reasoning_content` keeps
this a no-op outside thinking mode.
* chore: Trim noisy comments
Per CLAUDE.md ("self-documenting code; no inline comments narrating
what code does"), strip the multi-paragraph rationale that crept into
the DeepSeek reasoning_content fix. The commit history and PR
description carry the why; the code says the what.
Keeps one single-line JSDoc on `isDeepSeekReasoningProvider` (linking
to the DeepSeek docs) and a `(#13366)` tag on each opt-in site so
future readers can find the context.
* revert: Drop non-functional DeepSeek hint from OpenAI-compat serving paths
Codex's later review passes correctly flagged that threading the
DeepSeek formatter hint through openai.js (`/v1/chat/completions`) and
responses.js (`/v1/responses`) doesn't actually fix the second-turn
400 in those paths. Empirical check against the real SDK confirmed the
gap is deeper and pre-existing:
formatAgentMessages(payload, ..., { provider: DEEPSEEK })
where payload is the `convertMessages`/`convertInputToMessages` output
shape (string content + TOP-LEVEL `tool_calls`) produces NO tool-bearing
AIMessage at all — `formatAssistantMessage` only reconstructs tool calls
from `tool_call`-typed *content parts*, never a top-level `tool_calls`
field. So those serving paths don't reconstruct tool-call history (let
alone reasoning) regardless of the hint. The Responses persistence layer
likewise stores only output text, not tool calls or reasoning.
Making those paths work requires reworking the wire->internal message
conversion (and Responses persistence) to emit content-part arrays — a
broad, pre-existing concern beyond this issue and risky to land here.
Rather than ship a hint that looks like a fix but is inert, revert the
serving-path changes and scope this PR to the validated AgentClient
chat path (the actual surface in #13366).
Reverts the openai.js/responses.js threading and their spec mocks to
main. Keeps the AgentClient fix, `isDeepSeekReasoningProvider`, the
`getOpenAILLMConfig` flag, and the type.
* 📤 refactor: Align Mention Options With Model Selector (#13397)
* fix: Align mention endpoints with model specs
* test: Cover mention endpoint filtering
* fix: Respect added endpoints in mentions
* ♊ feat: Model-Aware Max Output Tokens for Google/Gemini (#13390)
* 📤 feat: Model-Aware Max Output Tokens for Google/Gemini
Resolves #13384.
Current Gemini text models (2.5 and 3+, including Gemini 3.5 Flash)
support 64K output tokens, but LibreChat defaulted every Google model
to the legacy 8K value — most visibly in the Agents model-parameter
panel.
- Add model-aware `reset`/`set` to `googleSettings.maxOutputTokens`,
mirroring the Anthropic pattern: Gemini 2.5/3+ -> 65536, legacy
(2.0 and earlier) and Gemma -> 8192.
- Resolve the default server-side in `getGoogleConfig` and in the
Agents, preset, and standard Google settings panels via a shared
`applyModelAwareDefaults` helper.
- Make `compactGoogleSchema` and `generateGoogleSchema` model-aware so
explicit user values are preserved and not overwritten.
* 🛡️ fix: Cap Google max output at Vertex-safe limits
Addresses Codex review (P1) on #13390. Vertex AI caps current Gemini
text models at 65,535 output tokens (vs 65,536 on AI Studio) and image
models at 32,768, so an unconditional 65,536 default could make
otherwise-default Vertex requests fail validation.
- Lower the modern text default/ceiling to 65535 (valid on both Vertex
and AI Studio).
- Resolve Gemini image models (e.g. gemini-2.5-flash-image) to 32768.
- Add reset/set + getGoogleConfig tests for image models and the Vertex
default path.
* 🧮 fix: Respect configured Google defaults and legacy image caps
Addresses Codex review round 2 on #13390 (one P2, two P3).
- P2 (llm.ts): apply the model-aware maxOutputTokens default as the final
fallback instead of pre-filling it, so an explicit value, `defaultParams`,
and `addParams` all take precedence and `dropParams` is honored. Empty-string
values stay stripped (preserves prior Gemini empty-payload handling).
- P3 (panels): pass the resolved params endpoint (`overriddenEndpointKey`) to
`applyModelAwareDefaults`, so custom endpoints with
`defaultParamsEndpoint: 'google'` also surface the model-aware default.
- P3 (schemas): nest the image-model check inside the 2.5+/3+ version check, so
legacy image IDs (e.g. gemini-2.0-flash-preview-image-generation) keep the 8K
cap instead of being treated as 32K models.
- Add tests for defaultParams precedence, dropParams, legacy image models, and
the Vertex default path.
* 🧭 fix: Base Google defaults on final model and configured overrides
Addresses Codex review round 3 on #13390 (two P2).
- llm.ts: resolve the model-aware maxOutputTokens default from the final
`llmConfig.model` (after defaultParams/addParams) instead of the model
captured from modelOptions, so a model forced via addParams/paramDefinitions
on a Google-compatible custom endpoint gets its correct limit.
- Panels: apply model-aware defaults to the built-in settings first, then
overlay `customParams.paramDefinitions`, so an admin-configured
maxOutputTokens default wins in the UI (consistent with backend precedence).
- Add parameterSettings.spec for applyModelAwareDefaults (incl. override
precedence) and a getGoogleConfig final-model test.
* ⚓ fix: Skip Retention for Persistent Agent Resource Files (#13394)
* 📦 chore: Update `@librechat/agents` to v3.1.99
* 📡 feat: Add Configurable HyperDX Browser Real User Monitoring (#13287)
* 🧟 fix: Reap Hung In-Memory Generations for Redis Failsafe Parity (#13396)
* ♻️ fix: Reap Stale In-Memory Generation Jobs to Prevent Heap OOM
InMemoryJobStore only reaped terminal jobs, so a generation that hung
without reaching completeJob() stayed "running" forever, retaining its
full message context. Abandoned jobs accumulated until the V8 heap was
exhausted (#13391). RedisJobStore already guards this with a 20-minute
running-job TTL; the in-memory store had no equivalent failsafe.
- Add a configurable staleJobTimeout (default 20m) to InMemoryJobStore;
cleanup() now reaps running jobs older than the timeout.
- Abort a pending generation in GenerationJobManager.cleanup() when its
job has been reaped, releasing client/graph references for GC.
- Abort the previous generation in createJob() when a job is replaced
for the same stream, closing an untracked-orphan leak.
- Forward staleJobTimeout through createStreamServices.
* 🩹 fix: Remove same-stream replacement abort (codex P1)
The createJob replacement-abort could let a stale, replaced request take
the abort-during-initialization path and complete/error the replacement
job via the shared streamId, and was a no-op across Redis replicas.
Removed it; the reported OOM is handled by the running-job failsafe and
the orphan-loop abort, which only fires when no job holds the streamId.
* 🩹 fix: Reap stale jobs on inactivity rather than age (codex P2)
Age-based reaping would drop a legitimately long but actively-streaming
generation at the timeout. Track a last-activity timestamp (refreshed on
each emitted chunk via recordActivity) and reap on inactivity instead,
mirroring RedisJobStore refreshing the running TTL on each appendChunk.
* 🩹 fix: Notify reaped streams and reset activity on replacement (codex P2)
- Emit a terminal error to any client still attached when a stale job is
reaped, so the SSE connection closes instead of hanging open with no
final/error event.
- Clear lastActivity in createJob so a replacement reusing the same
streamId falls back to its fresh createdAt and isn't reaped immediately
on the previous generation's stale activity timestamp.
* 🪪 feat: Accept Multiple OpenID JWT Audiences (#13404)
Adapted from ClickHouse/LibreChat@2e8f9f541d3616a89c75c8bc91d50fe1cae0cb16.
Co-authored-by: Alexey Korepanov <alexey.korepanov@clickhouse.com>
* 🧮 chore: Update Gemma Context Token Defaults (#13410)
* 🪃 fix: Retry MCP OAuth Token Refresh Without Scope on Server Rejection (#13412)
LibreChat sends `scope` on the refresh_token grant by default (PR #7924) because some authorization servers expect it. Salesforce rejects any scope on refresh with HTTP 400 "scope parameter not supported" (confirmed in production logs for case 00046259), which broke token refresh and forced re-authentication — amplifying the multi-replica PKCE retry storm.
RFC 6749 §6 makes scope optional on refresh (the server reuses the original grant). postRefreshRequest now sends scope as before and retries once WITHOUT it only when the failure is specifically a scope-parameter rejection (isScopeParameterRejection), so servers that need scope are unaffected and Salesforce-like servers self-heal with no operator config.
* 📦 chore: Bump `@librechat/agents` to v3.2.0
* 🖼️ fix: Preserve Model Spec Icon URLs (#13370)
* 🪪 feat: MCP OAuth - Support `audience` parameter for Auth0/Cognito-style providers (#13402)
* feat(mcp/oauth): support audience parameter for Auth0/Cognito-style providers
LibreChat already follows RFC 9728 (Protected Resource Metadata discovery)
and RFC 8707 (resource indicators on /authorize). However, authorization
servers that pre-date RFC 8707 — most prominently Auth0 — issue
API-scoped access tokens only when an Auth0-specific 'audience' parameter
is supplied on /authorize and /token. Without it, refresh_token responses
strip the API audience and the next MCP call 401s.
This change adds an optional 'audience' field to OAuthOptionsSchema and
forwards it on:
* pre-configured authorize URL build
* discovered (DCR + RFC 9728) authorize URL build
* refresh_token grant body
'resource' (RFC 8707) is left untouched and remains the
standards-conformant route; 'audience' covers providers that ignore
'resource'. The two are independent — providers may accept either, both,
or neither, so we forward whichever the operator configures.
Schema tests added; no behavioral change for existing configs (field is
optional with no default).
Refs: MCP Authorization Spec 2025-06-18, RFC 9728, RFC 8707.
* ci: build audience-fix branch image to ghcr.io/freudator86/librechat:audience-fix
* Revert "ci: build audience-fix branch image to ghcr.io/freudator86/librechat:audience-fix"
This reverts commit 7b3dfa6cd7c67354137284cb600d0fd00f4735b9.
* tests: assert audience param in authorize URL + refresh body; tighten schema (.min(1)); refine comment to reflect actual code paths
Adresses PR review:
- audience: z.string().min(1).optional() rejects empty strings
- schema comment now precisely lists the two code paths (authorize + refresh_token grant); explicitly notes the authorization_code exchange intentionally does not receive audience because Auth0 binds it from the initial /authorize request
- new MCPOAuthAudience.test.ts: 4 cases — authorize URL with/without audience, refresh body with/without audience — using a local recording HTTP server (no shared helper changes)
- new schema test: empty-string audience is rejected
* style: inline two logger.debug calls (prettier)
* style: inline third audience-debug log (prettier)
* feat(mcp/oauth): add forward_audience_on_refresh opt-out for strict token endpoints (Cognito)
Addresses Codex review P2 'Avoid sending audience on refresh grants':
the previous behavior forwarded audience on every refresh_token grant,
which is correct for Auth0 (strips the audience claim otherwise) but is
non-standard for Cognito and other strict OAuth 2.0 token endpoints that
document refresh as grant_type + client_id + refresh_token only.
New optional boolean 'forward_audience_on_refresh' (default: true)
preserves the existing Auth0-friendly default while letting operators
of strict tenants opt out cleanly. Schema + handler tests cover both
cases.
No behavioral change for existing configs.
* style: format MCP OAuth refresh audience log
---------
Co-authored-by: Tim Freudenthal <tim@allesknut.de>
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🔑 fix: support 'userinfo' in OPENID_REQUIRED_ROLE_TOKEN_KIND (#13182)
* fix: 'Key ... not found in userinfo token!' for OPENID_REQUIRED_ROLE_TOKEN_KIND
Added userinfo as option to: OPENID_REQUIRED_ROLE_TOKEN_KIND handler.
Added a small refactor to case match the OPENID_REQUIRED_ROLE_TOKEN_KIND
setting and throw an explicit error.
* Addressed review feedback and switched from case match to if/else
* Extracted a function to be called so Admin and User token use same code to resolve token types
* 🪪 fix: Preserve Trusted Registration Provider Overrides (#13307)
* 💠 feat: Extend `thinkingLevel` Support to Gemma 4 Models (#13088)
* ✂️ chore: Strip Session JWT Forwarding from Browser RUM (#13414)
* fix: disable RUM user JWT auth
* fix: remove stale RUM bootstrap import
* 💰 fix: Load app config in `set-balance` script to respect balance settings (#12669)
The `set-balance` script called `getBalanceConfig()` without the app
config, so it always reported balance as disabled regardless of the
librechat.yaml configuration. Mirror the working `add-balance` script
by loading the app config first and passing it into `getBalanceConfig`.
Fixes #12413
Co-authored-by: Claude <noreply@anthropic.com>
* 📬 fix: Honor Admin-Panel `allowedDomains` Override at Registration (#13204)
* fix: honor admin-panel allowedDomains override at registration
registerUser called getAppConfig({ baseOnly: true }), which short-
circuits before any DB override merge. As a result, admin-panel edits to
registration.allowedDomains were silently ignored at signup, even though
they correctly apply to SSO callbacks via checkDomainAllowed (which
calls getAppConfig() with the full resolution).
The admin panel writes registration.allowedDomains to the __base__
principal in the configs collection. That principal is unconditionally
injected by getApplicableConfigs (no user identity required), so a
fully-resolved getAppConfig call picks up the override even before any
user exists. This aligns native signup with the SSO paths and lets
admins tighten or relax the allowed list without a backend restart.
Per review feedback: pass the ALS tenantId explicitly. /api/auth runs
through preAuthTenantMiddleware, which puts a tenantId into
AsyncLocalStorage. Mongoose queries inside getApplicableConfigs are
ALS-scoped, but the per-principal merged-config cache key uses the
*explicit* tenantId parameter (see overrideCacheKey in
packages/api/src/app/service.ts). If we leave tenantId undefined while
ALS holds tenant A, the merged result caches at `__default__` — and a
later request from tenant B would hit that entry, leaking tenant A's
allowedDomains (and balance) across tenants. Reading getTenantId() and
forwarding it makes the cache key match the DB scope, so __base__
overrides apply per-tenant correctly.
Behavior when no admin override exists is unchanged (the merged config
equals the YAML config; optional chaining handles missing fields).
Tests in AuthService.spec.js:
- Regression guard that getAppConfig is called with `{}` (no baseOnly)
when ALS has no tenant — protects against reintroduction of the
short-circuit.
- New tenant-context test verifying getAppConfig({ tenantId }) when
getTenantId() returns a tenant ID — protects against cross-tenant
cache bleed.
- Behavioral test confirming a disallowed domain returns 403 before any
DB user lookup.
* test: remove unused registerSchema import after merge resolution
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* 📦 chore: Bump `@hyperdx/browser` to v0.24.0 (#13416)
- Update dependencies for @hyperdx/otel-web to 0.18.0 and @hyperdx/otel-web-session-recorder to 2.0.0
- Upgrade @hyperdx/instrumentation-exception to 0.3.0 and its dependencies
- Adjust peer dependencies and engine requirements for compatibility
* 🔧 chore: Drop duplicate `getAppConfig` import in `set-balance.js` (#13417)
#12669 added `const { getAppConfig } = require('~/server/services/Config');` near the top of `config/set-balance.js` but the same import already existed lower in the file, producing:
config/set-balance.js
9:9 error 'getAppConfig' is already defined no-redeclare
The fork's sync CI surfaced this when its pre-commit hook ran eslint on the merged file. The upstream `eslint-ci.yml` is path-filtered on `api/**`, `client/**`, and `packages/**` — none of which match `config/**`, which is why CI didn't catch it upstream.
Drop the second declaration. Functionally identical, lint clean. No other changes.
* 🔒 fix: Strip post-login fields from unauthenticated /api/config response (#13102)
* 🔒 fix: Strip post-login fields from unauthenticated /api/config response
Follow-up to #12490 reported in #12688.
The unauthenticated /api/config response still included fields that are
only consumed after login (helpAndFaqURL, sharedLinksEnabled,
publicSharedLinksEnabled, showBirthdayIcon, analyticsGtmId,
openidReuseTokens, allowAccountDeletion, customFooter, cloudFront).
None of these are read by the auth pages (Login, Registration,
RequestPasswordReset, ResetPassword, VerifyEmail, TwoFactorScreen,
AuthLayout, Footer, SocialLoginRender).
Split buildSharedPayload into two helpers:
- buildPreLoginPayload returns only the fields the unauthenticated auth
pages need (appTitle, server domain, social-login flags, OpenID/SAML
labels and image URLs, registration/email/password-reset flags,
minPasswordLength, ldap).
- buildPostLoginPayload returns the post-login informational fields and
is merged into the response only when req.user is present.
Also move buildCloudFrontStartupConfig into t…
Pull Request Template
Summary
This PR adds AWS profile support for Bedrock credentials, enabling automatic credential refresh using the AWS SDK's credential provider chain. This
improvement allows users to configure AWS profiles with optional
credential_processfor seamless token rotation, eliminating the need to manuallyupdate static environment variables when credentials expire.
Key Changes:
BEDROCK_AWS_PROFILEenvironment variable support.env.examplewith comprehensive documentation on profile configuration and optional auto-refresh setupBenefits:
Change Type
Please delete any irrelevant options.
Testing
Please describe your test process and include instructions so that we can reproduce your test. If there are any important variables for your testing configuration, list them here.
Test Configuration:
Environment Variables:
BEDROCK_AWS_DEFAULT_REGION=us-west-2 BEDROCK_AWS_PROFILE=my-bedrock-profile AWS Config (~/.aws/config): [profile my-bedrock-profile] region = us-west-2 credential_process = my-custom-refresh-command --format json # This can also be credential_process = node /path/to/LibreChat/config/get-aws-credentials.js or anything else...Test Steps:
Backward Compatibility Testing:
Checklist
Please delete any irrelevant options.
Test Suites: 1 skipped, 80 passed, 80 of 81 total