Commit graph

665 commits

Author SHA1 Message Date
Danny Avila
983a33fbad
🎛️ refactor: Scope App-Config Override Cache by Isolation Context (#13455)
getAppConfig caches per-principal merged config overrides under a key built by
overrideCacheKey(role, userId, tenantId). The key used the tenantId *argument*
only — but callers that go through the tenant middleware (the common path)
pass no explicit tenantId and rely on the AsyncLocalStorage tenant context.
Those calls were keyed under the shared '__default__' bucket, so the DB query
(correctly scoped to the ALS tenant by the Mongoose plugin) produced a merged
config that was then cached and served to the next tenant resolving the same
role/user — leaking model specs, endpoints, and interface flags across tenants.

Fall back to getTenantId() before '__default__' so the cache key reflects the
actual tenant scope (param or ALS). Tighten the strict-mode warning to fire
only when there is genuinely no tenant anywhere (param nor ALS), since the ALS
case is now scoped rather than defaulted. No-op for single-tenant deployments,
where getTenantId() is undefined and the key stays '__default__'.

Adds tests (real Map-backed cache) proving the ALS tenant scopes the key and
that two tenants resolving the same role each get their own config with no
cache collision.
2026-06-01 18:00:53 -04:00
Danny Avila
fb282a2afa
🐳 chore: Upgrade Docker Builds To Node 24 (#13448)
* chore: upgrade docker builds to node 24

* test: avoid array at in telemetry spec
2026-06-01 10:03:18 -04:00
Danny Avila
479e9d59b7
🧠 refactor: Memoize MCP Permission Checks Per Request (#13419) 2026-05-30 18:32:06 -04:00
Danny Avila
100871c3ec
🛂 fix: Enforce MCP Permissions for Agent Tools (#13174)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* fix: Enforce MCP Permissions for Agent Tools

* fix: Measure MCP Image Limit by Decoded Size

* fix: gate cached MCP tools and tighten remote image URL detection

Addresses Codex review findings on the MCP permissions PR:

- filterAuthorizedTools previously fast-accepted any tool present in the
  global tool cache before reaching the MCP-use permission gate. App-level
  MCP tools (keyed `name_mcp_server` by MCPServerInspector and merged into
  the cache via mergeAppTools) therefore bypassed the canUseMCP check,
  letting a user without MCP_SERVERS.USE persist/bind them. Route all
  MCP-delimited tools through the permission + server-access gate
  regardless of cache presence.

- assertImageDataWithinLimit / image formatter used startsWith("http")
  to skip the size cap, which also matched base64 payloads that happen to
  begin with those chars. Require http:// or https:// via a shared
  isRemoteImageUrl helper so oversized inline base64 can no longer bypass
  MCP_IMAGE_DATA_MAX_BYTES.

Adds regression tests for both paths.

* fix: address Codex round-2 findings on MCP permissions PR

- parsers.ts: parseAsString dropped the image payload for unrecognized
  providers, returning only `Image result: <mimeType>`. Pre-PR these
  items survived via JSON.stringify(item). Keep the size guard but fall
  through to JSON.stringify so the data/URL is preserved.

- MCP.js: the runtime MCP-use check only read `configurable.user`, so
  paths that propagate `user_id` only (e.g. the OpenAI-compatible API in
  agents/openai/service.ts) rejected every MCP tool call for an
  authenticated user. Add resolveMCPPermissionUser: use the safe user
  directly when it already carries a role (no extra DB call), otherwise
  fall back to loading the role by user_id. Update fail-closed tests to
  the resolved behavior.

- v1.js: the update path only re-filtered newly added MCP tools, so a
  user who lost MCP_SERVERS.USE kept existing MCP bindings on edit while
  create/duplicate/revert stripped them. Strip all MCP tools on update
  when the permission is revoked; keep the narrower new-tool gating (and
  disconnect/registry preservation) when it is intact.

Updates and adds regression tests for all three paths.

* fix: populate safe user at producer instead of resolving in runtime MCP check

Corrects the Finding B approach from the previous commit. Rather than
loading the user by id inside the runtime MCP permission check, populate
`configurable.user` (and createRun's `user`) with the full safe user at
the producer, matching the in-repo agent controllers
(responses.js / openai.js) which already pass `createSafeUser(req.user)`.

- service.ts: derive `safeUser` via createSafeUser(req.user) and pass it
  to both createRun and processStream's configurable, so the role-bearing
  identity reaches the runtime `userCanUseMCPServers(configurable.user)`
  check. Falls back to a bare id when the host app attached no user,
  which correctly leaves MCP gated (fail closed).
- MCP.js: revert the resolveMCPPermissionUser DB-load fallback; the
  runtime check again reads configurable.user directly and fails closed
  when absent (defense in depth).
- MCP.spec.js: revert to the matching runtime test expectations.

* test: cover safe-user propagation in createAgentChatCompletion

Adds a focused spec for the OpenAI-compatible chat completion service
(the producer fixed for Codex Finding B). Injects mocked deps and asserts
that createRun and processStream's configurable.user carry the role from
req.user (with sensitive fields stripped by createSafeUser), and that an
unauthenticated request falls back to a bare { id: 'api-user' } so the
runtime MCP check fails closed.

* fix: address Codex round-3 findings + TS6133

- MCP.js (P1): the assistants required-action path invokes tool._call(
  toolInput) with no LangChain config, so the runtime check saw no
  configurable.user and rejected authorized users. createToolInstance now
  captures the creation-time user (req.user via createMCPTool) and _call
  falls back to it for both the permission check and userId. Still fails
  closed when neither config nor captured user carries a role.

- v1.js (P2): the update-path isMCPTool used a bare mcp_delimiter substring
  check, misclassifying action tools whose operationId contains "_mcp_"
  (e.g. sync_mcp_state_action_...) as MCP and dropping them on a
  permission-revoked edit. Delegate to the canonical isActionTool so only
  real MCP tools are gated. Regression test added.

- service.ts: drop the now-unused IUser import (TS6133); derive reqUser's
  type from createSafeUser's own parameter instead.

* fix: resolve TS7022 self-reference in service.spec mock res

The mock response object referenced `res` inside its own `status`/`json`
initializers without a type annotation, so tsc inferred `res` as `any`
(TS7022). Annotate the object and assign the self-referencing chainable
methods after declaration.

* fix: correct round-4 findings (isActionTool import, captured user, partial-update)

- v1.js: import isActionTool from librechat-data-provider (its real export;
  @librechat/api does not export it, so the prior import was undefined and
  threw TypeError). Exclude action tools from MCP classification in both the
  main filterAuthorizedTools loop and the update path, so action tools whose
  operationId contains _mcp_ (e.g. sync_mcp_state_action_...) are preserved
  regardless of MCP permission.
- v1.js: evaluate the effective tool set (updateData.tools ?? existingAgent.tools)
  so a tools-less PATCH by a user who lost MCP_SERVERS.USE still strips stale
  MCP bindings, matching create/duplicate/revert.
- MCP.js: createToolInstance now receives the construction-time user and _call
  falls back to it (permissionUser) when configurable.user is absent, fixing the
  assistants required-action path that invokes _call without a config and
  resolving the capturedUser no-undef/ReferenceError.
- Tests: action-tool preservation (authorized + denied), tools-less revocation
  PATCH, updated revocation test to expect all MCP tools stripped.

Affected specs pass locally: MCP 49/49, filterAuthorizedTools 49/49.

* fix: guard isActionTool against non-string tools; correct actionDelimiter import

Two test regressions from the prior commit:
- The main filterAuthorizedTools loop called isActionTool(tool) directly,
  but isActionTool does toolName.indexOf(...) and throws on null/undefined.
  Compute isActionToolName = typeof tool === 'string' && isActionTool(tool)
  once and reuse it, restoring graceful null/undefined handling.
- The action-tool test referenced Constants.actionDelimiter (undefined);
  actionDelimiter is a standalone librechat-data-provider export. Import and
  use it directly.

filterAuthorizedTools 36/36 and MCP 40/40 pass locally.

* fix: address MCP permission review follow-ups

* fix: preserve shared agent MCP tools
2026-05-30 16:19:49 -04:00
Danny Avila
5bfef51ed2
🏟️ fix: Restrict MCP OAuth Audience in User-Managed Configs (#13418)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
2026-05-30 14:39:54 -04:00
King-of-Infinite-Space
b61d5377ef
💠 feat: Extend thinkingLevel Support to Gemma 4 Models (#13088) 2026-05-30 10:16:27 -04:00
Freudator86
c6a6f2e3ae
🪪 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 7b3dfa6cd7.

* 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>
2026-05-30 06:59:39 -07:00
Danny Avila
a83bcb3490
🪃 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.
2026-05-30 01:52:43 -04:00
Danny Avila
cb6bd71ab9
🧮 chore: Update Gemma Context Token Defaults (#13410) 2026-05-30 00:29:19 -04:00
Danny Avila
a14344ded7
🧟 fix: Reap Hung In-Memory Generations for Redis Failsafe Parity (#13396)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* ♻️ 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.
2026-05-29 12:07:13 -07:00
Danny Avila
06a6c42435
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.
2026-05-29 08:09:32 -07:00
Danny Avila
6d9c01927d
🧠 refactor: Replay DeepSeek reasoning_content via OpenRouter (#13368)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* 🧠 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.
2026-05-28 22:10:49 -07:00
Danny Avila
62dff69300
🧠 feat: Add Claude Opus 4.8 Support (#13380)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 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
2026-05-28 13:50:39 -07:00
Danny Avila
0d981b08d8
🪡 fix: Artifact Edit Saves (#13358)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* 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
2026-05-27 22:03:42 -07:00
Danny Avila
f95fa55cce
📎 fix: Preserve Gemini PDF Media Blocks (#13357) 2026-05-27 22:00:53 -07:00
Danny Avila
abfe7d19f4 🪢 fix: Coerce Tool Execution Args By Schema (#13310)
* fix: Coerce tool execution args by schema

* 📦 chore: bump `@librechat/agents` to v3.1.95
2026-05-26 15:33:59 -07:00
Danny Avila
f2be5baecf
🧵 fix: Prevent Message Loading Race During Streaming (#13295) 2026-05-24 18:50:00 -04:00
Serhii Zghama
9cd2fc2ed6
🧩 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>
2026-05-24 14:49:40 -04:00
Danny Avila
83c7d637c3
🎨 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.
2026-05-23 17:52:58 -04:00
Dustin Healy
2bcf3e8582
🪟 fix: Apply Admin-Panel Config Overrides To YAML-Defined MCP Servers (#13173)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🪟 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.
2026-05-23 17:11:13 -04:00
Danny Avila
4f5486c932
🛰️ 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
2026-05-23 16:55:57 -04:00
Danny Avila
1693b586d4
🏣 fix: Reject System Tenant In Auth Context (#13278) 2026-05-23 16:55:32 -04:00
Danny Avila
5071b2b617
🧷 fix: Pin MCP OAuth Client Secrets (#13276)
* fix: Pin MCP OAuth client secrets

* fix: Require MCP OAuth client id for secrets
2026-05-23 16:51:45 -04:00
Danny Avila
af902118c9
🧱 fix: Validate Bedrock User Credentials (#13277) 2026-05-23 16:46:15 -04:00
Danny Avila
7cd467a528
🧷 fix: Harden MCP Proxy SSRF Checks (#13274) 2026-05-23 16:30:13 -04:00
Dustin Healy
53e7c41033
🪙 feat: Add AWS Bedrock API key support (#8690)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
* 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>
2026-05-23 12:41:38 -04:00
Maxence Dominici
294bf7c87d
🛂 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>
2026-05-23 10:39:11 -04:00
Serhii Zghama
7f0fde2d98
🪨 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.
2026-05-23 09:43:40 -04:00
Danny Avila
6d6ea08da4
🆔 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
2026-05-23 09:41:13 -04:00
Max
1746153c17
🪬 fix: Skip MCP Tools When Required Custom User Vars Are Unset (#13152)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 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>
2026-05-23 09:24:29 -04:00
janluedemann-esome
abda15f4eb
🛂 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
2026-05-23 09:09:13 -04:00
apuzikov
fb851cae63
🪪 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
2026-05-23 09:01:44 -04:00
Dev Chohan
01af63cb52
fix: Use JWT exp claim for MCP when OAuth token omits expires_in (#13248)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
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>
2026-05-22 21:20:59 -04:00
Ravi Kumar L
03b477a84c
🖲️ feat: Trace SSE Stream Lifecycle with OTel (#13266) 2026-05-22 21:19:45 -04:00
Danny Avila
2ce3921f78
⚖️ 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
2026-05-22 20:47:41 -04:00
Danny Avila
bd64251eb9
🪪 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
2026-05-22 20:46:14 -04:00
Danny Avila
d462bf4113
🪪 fix: Consolidate MCP OAuth Policy (#13254)
* 🦉 feat: Implement proactive OAuth flow for connections without stored tokens

* 🤝 fix: Enhance proactive OAuth flow handling in MCPConnectionFactory

* fix: Add timeout handling for proactive OAuth flow in MCPConnectionFactory

Co-authored-by: Copilot <copilot@github.com>

* fix: Refine proactive MCP OAuth flow

* test: Cover proactive OAuth missing handler

* fix: Require explicit MCP OAuth signal

* fix: Consolidate MCP OAuth policy

---------

Co-authored-by: Gil Assunção <gil.assuncao@parceiros.nos.pt>
Co-authored-by: Copilot <copilot@github.com>
2026-05-22 20:43:34 -04:00
Danny Avila
c1e071b7a0
🛡️ fix: Harden MCP OAuth Request Handling (#13264)
* fix: Harden MCP OAuth request handling

* fix: Bound MCP OAuth dispatcher cache

* fix: Harden OAuth DNS lookup handling
2026-05-22 20:39:16 -04:00
Danny Avila
b9d4a595b9
🗂️ refactor: Clarify Code Sandbox File Guidance (#13236) 2026-05-21 21:13:06 -04:00
Danny Avila
cfe0f9f7f6
📡 fix: Respect Custom Endpoint Stream Usage Opt-In (#13237) 2026-05-21 15:39:57 -04:00
Danny Avila
05a3d1ed81
🛣️ feat: Add MCP Remote Proxy Support (#13076)
* feat: add MCP remote proxy support

* fix: Harden MCP Proxy Review Findings

* fix: Honor MCP Proxy Env Precedence

* fix: Harden MCP proxy routing

* fix: Align MCP proxy bypass semantics

* test: Pin MCP proxy admin scope
2026-05-21 15:28:54 -04:00
Danny Avila
12a44120f8
feat: Add Gemini 3.5 Flash Support (#13231)
* feat: add Gemini 3.5 Flash support

* fix: refine Gemini 3.5 Flash overrides

* fix: satisfy Gemini thinking config types

* fix: drop empty Gemini thinking config
2026-05-21 14:18:34 -04:00
Danny Avila
cbdfe4614b
🏃 fix: Improve OpenID Lookup Planning (#13229)
* fix: improve OpenID lookup planning

* fix: add issuer-bound source id index

* fix: align OpenID source id index

* fix: preserve admin refresh recency
2026-05-21 14:17:55 -04:00
Danny Avila
f34150e8e8
🧵 chore: Raise MCP SSE Line Default (#13224)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
2026-05-21 09:06:02 -04:00
Danny Avila
1ed84ee4eb
🦣 fix: Response Size Limits for Streamable HTTP MCP Responses (#13219)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* fix: implement response size limits for streamable HTTP MCP responses

- Added environment variables for maximum response and line sizes in streamable HTTP responses.
- Introduced functions to handle response size validation and error logging.
- Updated MCP connection logic to enforce these limits, ensuring safe handling of large responses.

* fix: address MCP response guard review findings

* fix: satisfy logger spy typings

* fix: clarify blocked MCP response errors

* fix: harden MCP guard review edge cases

* test: cover MCP oversized SSE call failures
2026-05-21 01:29:43 -04:00
Pete Hampton
679672ad15
🪂 feat: Graceful HTTP shutdown on SIGTERM/SIGINT (#13211)
* 🪂 feat: Graceful HTTP shutdown on SIGTERM/SIGINT

* Address feedback

* don't treat ERR_SERVER_NOT_RUNNING as fatal; route telemetry shutdown through coordinator
2026-05-20 13:33:53 -04:00
Danny Avila
9dd062e42e
🧯 fix: Harden Data Retention Semantics (#13049)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* feat: support data retention for normal chats

Add retentionMode config variable supporting "all" and "temporary" values.
When "all" is set, data retention applies to all chats, not just temporary ones.
Adds isTemporary field to conversations for proper filtering.

Adapted to new TS method files in packages/data-schemas since upstream
moved models out of api/models/.

Based on danny-avila/LibreChat#10532

Co-Authored-By: WhammyLeaf <233105313+WhammyLeaf@users.noreply.github.com>
(cherry picked from commit 30109e90b0)

* feat: extend data retention to files, tool calls, and shared links

Add expiredAt field and TTL indexes to file, toolCall, and share schemas.
Set expiredAt on tool calls, shared links, and file uploads when
retentionMode is "all" or chat is temporary.

(cherry picked from commit 48973752d3)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: lint/test

(cherry picked from commit 310c514e6a)

* fix: address code review feedback for data retention PR

Critical:
- Fix BookmarkMenu crash: restore optional chaining on conversation
- Fix migration hazard: backward-compatible sidebar filter that also
  checks expiredAt for documents without isTemporary field

Major:
- Add logging to getRetentionExpiry error path, align with tools.js
- Add tests for retentionMode: ALL in saveConvo and saveMessage
- Fix share route: apply expiredAt for temporary chats too by
  querying the conversation's isTemporary flag server-side
- Add assertions for getRetentionExpiry mocks in process tests

Minor:
- Fix ChatRoute isTemporaryChat to be strictly boolean via Boolean()
- Fix stale test description (expired -> temporary)
- Comment out retentionMode default in example yaml
- Simplify verbose if/else to isTemporary === true
- Add compound index on { user: 1, isTemporary: 1 }
- Remove narrating comment from process.spec.js

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
(cherry picked from commit 6bad535f90)

* chore: fix typescript

(cherry picked from commit 826527a46b)

* fix: lint

(cherry picked from commit 77817e80ea)

* fix: use mockSanitizeArtifactPath in retention test

The 'getRetentionExpiry is called with the request object' test
referenced an undefined `mockSanitizeFilename` identifier, breaking
both lint (no-undef) and the test suite. Use the existing
`mockSanitizeArtifactPath` mock that the surrounding tests already
use, since `processCodeOutput` calls `sanitizeArtifactPath` (not
`sanitizeFilename`) before invoking `getRetentionExpiry`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 52ea2da66d)

* fix: forward isTemporary from client for retention on file uploads and tool calls

Server-side `getRetentionExpiry` (file uploads) and the tool-call
controller both read `req.body.isTemporary`, but the file upload
multipart form and the tool-call payload did not include that field.
In `retentionMode: temporary` (default), files uploaded and tool
calls created from temporary chats were therefore retained
indefinitely.

Forward the Recoil `isTemporary` flag in both client paths so the
existing server checks can fire correctly. `ToolParams` gains an
optional `isTemporary` field.

Addresses Codex P1 review feedback on PR #29.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 7e937df05a)

* test: stub store.isTemporary in useFileHandling test mocks

Previous commit added `useRecoilValue(store.isTemporary)` to the
hook. The test file mocks `~/store` with only `ephemeralAgentByConvoId`
and does not stub `useRecoilValue`, so all 7 cases threw
"Invalid argument to useRecoilValue: expected an atom or selector but
got undefined". Add a stub default export with `isTemporary` and a
`useRecoilValue` mock returning `false`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit eb1609537d)

* fix: harden data retention semantics

* fix: provide sweep request context for expired files

* fix: preserve temporary flags in all-retention updates

* fix: honor assistant versions in retention sweeps

* fix: retain non-temporary flags in all mode

* fix: hide expired retained records

* fix: propagate retained conversation expiry

* fix: refresh meili retention cutoff

* fix: prevent overlapping file sweeps

* fix: show legacy retained conversations

* fix: index legacy retained records

* fix: harden retention cleanup edge cases

* fix: count failed file storage sweeps

* fix: preserve legacy temporary retention

* fix: assign retention sweep worker deterministically

* fix: hide expired shared links on reads

* fix: prevent retention refresh after parent expiry

* fix: break code output retention import cycle

* fix: harden retention review findings

* fix: ignore expired share duplicates

* fix: reject expired retained share creation

* fix: harden retention review edge cases

* fix: address retention audit findings

* fix: enforce expired conversation shares in all retention

* fix: scope temporary upload flag to chat files

* fix: address retention review findings

* fix: address codex retention review findings

* fix: tighten missing storage detection

* test: remove unused file process spec bindings

---------

Co-authored-by: WhammyLeaf <233105313+WhammyLeaf@users.noreply.github.com>
Co-authored-by: Aron Gates <aron@muonspace.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 21:58:42 -04:00
Peter Nancarrow
2418f854df
📡 fix: Handle Pre-Session 406 for Optional SSE MCP Stream (#13202) 2026-05-19 20:59:45 -04:00
Danny Avila
749eb06e67
🧭 fix: Reduce MCP Registry ACL Lookups (#13195) 2026-05-19 17:16:37 -04:00
Danny Avila
2414e9c7d2
🛟 refactor: Gracefully Skip Unavailable Web Search Rerankers (#13191)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
2026-05-19 09:48:12 -04:00