🪡 fix: Handle Missing Skill File Upsert Metadata#13520
Conversation
|
@codex review |
There was a problem hiding this comment.
Pull request overview
This PR hardens the skill-file upload/save workflow by preventing crashes and storage leaks when the skill-file metadata upsert unexpectedly returns no saved row. It also makes insert-vs-replace detection for SkillFile atomic by consuming MongoDB modify-result metadata from the same upsert operation that returns the saved document.
Changes:
- Updated
upsertSkillFileto useincludeResultMetadataand throw a dedicatedSKILL_FILE_UPSERT_NOT_FOUNDerror when the modify-result has novalue. - Guarded
saveSkillFileContentagainst null metadata-upsert results and ensured newly uploaded objects are deleted on failure. - Added regression tests covering null upsert results and ensuring skill counters/blob cleanup behave correctly.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| packages/data-schemas/src/methods/skill.ts | Uses MongoDB modify-result metadata to atomically detect insert vs replace and throws explicit error if upsert yields no saved row. |
| packages/data-schemas/src/methods/skill.spec.ts | Adds a regression test asserting explicit error + no parent-skill bump when upsert returns value: null. |
| api/server/services/Endpoints/agents/skillDeps.js | Adds a null-result guard for metadata upsert and triggers cleanup of the newly uploaded object on failure. |
| api/server/services/Endpoints/agents/skillDeps.spec.js | Adds a regression test ensuring uploaded-object cleanup occurs when metadata upsert returns no row. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Codex Review: Didn't find any major issues. Swish! ℹ️ 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". |
GitNexus: 🚀 deployedThe |
* 🌍 i18n: Update translation.json with latest translations (danny-avila#13505) * 🎭 test: Run Mock E2E Suite Through `createRun` With In-Process Fake Model (danny-avila#13508) * 🎭 test: Run Mock E2E Suite Through createRun With In-Process Fake Model Replace the standalone HTTP mock LLM server with an in-process fake model injected into the real createRun -> Run.create pipeline via run.Graph.overrideTestModel, so the mock suite exercises the agents integration end-to-end without a live provider or a separate server. - Bump @librechat/agents to 3.2.2 for the FakeChatModel/createFakeStreamingLLM exports - Add an env-gated applyTestRunHook seam in packages/api createRun (no /api changes) - Add e2e/setup/fake-model.js to drive default replies + the skill-authoring tool-call flow - Drop the mock-llm webServer from playwright.config.mock.ts and set LIBRECHAT_TEST_RUN_HOOK * 🧹 test: Retire Standalone Mock LLM Server From E2E Recorder Migrate the `--profile=mock` recorder onto the same in-process fake model as the Playwright mock suite, then delete the now-unused HTTP mock server so the fake-LLM logic lives in a single place. - Point record.js mock profile at the fake model via LIBRECHAT_TEST_RUN_HOOK - Remove the mock-llm-server spawn/wait and MOCK_LLM_PORT plumbing from record.js - Delete e2e/setup/mock-llm-server.js (e2e/setup/fake-model.js is now the only source) - Update e2e/README.md to describe the in-process fake LLM * 🏷️ ci: Rename Playwright Mock E2E Check to Playwright E2E Tests * 🧷 fix: Bind Agent File Context to Current Turn (danny-avila#13506) * fix: Bind agent file context to current turn * fix: Avoid duplicating agent file context * fix: Export agent file context prepender * test: Use exported file context prepender * fix: Keep file context transient for memory and counts * 💼 fix: Harden Shared-Link Message Sanitization with an Allowlist (danny-avila#13510) * fix: harden shared-link message sanitization with an allowlist Public shared links built their message payload via `{ ...message }` and `{ ...attachment }` spreads, which exposed internal fields that are never needed by the shared view: - message: endpoint, conversationSignature, clientId, plugin(s), metadata - attachments: filepath, storageKey, metadata, and other internal keys Replace the passthrough with an allowlist so only render-relevant fields (sender, text, content, token/feedback/error flags, and sanitized attachments) are surfaced. Assistant model ids remain anonymized; other model names are omitted rather than disclosed. Also add `tenantId?: string` to ISharedLink, matching the field already read by the shared-link access middleware for multi-tenant deployments. * fix: preserve shared render data; sanitize by denylist (review feedback) Address Codex/Copilot review on the shared-link sanitization: - The tight attachment allowlist dropped tool-call render data. Switch to a denylist of storage/identity-internal fields (filepath, storageKey, user, tenantId, source, metadata, …) so toolCallId, tool payloads (web_search / file_search / etc.), and dimensions are preserved while internals are stripped. - Preserve user-uploaded message.files (previously dropped entirely) via the same denylist sanitizer, so shared links keep uploaded images/documents. - Introduce a dedicated SharedMessage / SharedFile type and use it for SharedMessagesResult.messages instead of casting the allowlisted object to IMessage, so omitted fields are caught at compile time. Extends the regression test to assert toolCallId, web_search payload, and files survive while filepath/storageKey/user/tenantId/metadata are removed. * fix: keep render URLs + skill badges, drop private feedback (review round 2) Address Codex round-2 findings on shared-link sanitization: - filepath is the URL the share renderer loads (Files.tsx uses file.preview ?? file.filepath; image attachments render only when filepath is set). Drop it from the denylist so shared images/downloads still render; storageKey (the raw object key) stays stripped. - Preserve manualSkills / alwaysAppliedSkills so SkillPills still render for skill-assisted turns (non-sensitive UI metadata). - Remove feedback from the shared projection — it is the owner's private rating/notes, is never rendered in the share view, and must not be exposed to anyone holding the share URL. Regression test updated to assert filepath/skills survive and feedback is omitted. * fix: anonymize file ids + preserve message iconURL (review round 3) - Persisted message.files records can carry the original conversationId/messageId. Attachments were already rewritten to the anonymized ids; apply the same rewrite to files so shared user-uploaded files don't expose the real ids. - Preserve message.iconURL (read by Share/MessageIcon.tsx) so shared assistant/ custom-endpoint turns keep their custom avatar instead of falling back to the generic icon. Regression test asserts the shared file's conversationId is the anonymized id and that iconURL survives. * 🖼️ fix: Upgrade Framer Motion for Vite 8 Compatibility (danny-avila#13512) LibreChat recently updated Vite (see 7dba640). The older version of framer-motion we're using is incompatible with this newer version of Vite; if you try to use it, you get the error "e is not a function." (One easy way to reproduce: try to enable 2FA on your account.) Updating to the latest framer-motion fixes this issue. * 🧪 test: Add E2E Regression For 2FA framer-motion Crash (danny-avila#13513) Add a Playwright mock e2e spec that opens Settings -> Account -> Enable 2FA and asserts the framer-motion dialog renders. Reproduces the Vite / framer-motion incompatibility from issue danny-avila#13511: on the current build the dialog crashes the client with "e is not a function" and never renders, so this spec fails until the framer-motion bump in danny-avila#13512 is merged. * 🪧 chore: Generalize Project Name Placeholder (danny-avila#13514) * 📦 chore: npm audit fix (danny-avila#13515) - Upgraded @langchain/langgraph from 1.3.2 to 1.3.4 - Upgraded @langchain/langgraph-checkpoint from 1.0.2 to 1.0.4 - Upgraded @langchain/langgraph-sdk from 1.9.4 to 1.9.15 - Updated uuid from 10.0.0 to 14.0.0 across multiple packages - Upgraded @langchain/protocol from 0.0.15 to 0.0.16 - Upgraded @remix-run/router from 1.23.2 to 1.23.3 - Upgraded hono from 4.12.18 to 4.12.23 - Upgraded react-router from 6.30.3 to 6.30.4 * 📜 feat: Improve Skill Authoring Guidance (danny-avila#13517) * feat: Improve skill authoring guidance * test: Guard tool description lengths * fix: Align skill template guidance * fix: Satisfy advisory limit test lint * fix: Transform LangGraph ESM in Jest * 🪡 fix: Handle Missing Skill File Upsert Metadata (danny-avila#13520) --------- Co-authored-by: Danny Avila <danny@librechat.ai> Co-authored-by: Dan Lew <dlew@users.noreply.github.com>
* 🪧 chore: Generalize Project Name Placeholder (#13514)
* 📦 chore: npm audit fix (#13515)
- Upgraded @langchain/langgraph from 1.3.2 to 1.3.4
- Upgraded @langchain/langgraph-checkpoint from 1.0.2 to 1.0.4
- Upgraded @langchain/langgraph-sdk from 1.9.4 to 1.9.15
- Updated uuid from 10.0.0 to 14.0.0 across multiple packages
- Upgraded @langchain/protocol from 0.0.15 to 0.0.16
- Upgraded @remix-run/router from 1.23.2 to 1.23.3
- Upgraded hono from 4.12.18 to 4.12.23
- Upgraded react-router from 6.30.3 to 6.30.4
* fix: export sanitizeSchemaMetadata from @librechat/api utils
* 📜 feat: Improve Skill Authoring Guidance (#13517)
* feat: Improve skill authoring guidance
* test: Guard tool description lengths
* fix: Align skill template guidance
* fix: Satisfy advisory limit test lint
* fix: Transform LangGraph ESM in Jest
* 🪡 fix: Handle Missing Skill File Upsert Metadata (#13520)
* fix: add check 0f and rebuild-before-verify to close stale-dist gap in check 0d
The v0.8.6 merge dropped 'export * from ./schema' from
packages/api/src/utils/index.ts. This caused sanitizeSchemaMetadata to be
undefined at runtime, crashing the server on startup.
Root cause: check 0d validates the compiled @librechat/api dist, not the
source. The dist from before the merge was still correct, so check 0d
silently passed even though the source export had been dropped. The bug
was only caught at runtime after running the backend.
Two fixes:
1. verify-paychex-customizations.sh: add check 0f — validates the SOURCE
of packages/api/src/utils/index.ts directly, confirming the Paychex-
added 'export * from ./schema' re-export is present. Catches the drop
before a rebuild exposes it at runtime.
2. upstream-merge.agent.md: step 10 now requires 'npm run build:api' before
'./scripts/verify-paychex-customizations.sh', with an explanation of why
the order matters. Added Don't (run check 0d against stale dist) and Do
(run build:api first) to the pitfalls section.
* 👻 fix: Clear Project-Scoped Landing When the Selected Project Is Deleted (#13525)
* fix(projects): clear landing scope when the selected project is deleted
When a project-scoped new-chat landing (/c/new?projectId=...) was open and the
project got deleted, the chip kept showing the dead project and sends targeted it
(saving unscoped with a visual glitch).
- ChatRoute: only trust the scope when the project query succeeds (isSuccess), so
React Query's retained-on-error data can't keep a deleted project's chip alive;
strip ?projectId once the query settles to not-found so the landing reverts to a
normal unscoped chat.
- useDeleteProjectMutation: invalidate the project-detail query instead of removing
it, so active observers refetch and settle into an error state (removing left them
stuck loading under refetchOnMount: false).
- e2e: regression test for delete-while-scoped.
Fixes a follow-up issue to the projects feature (#13467).
* fix(projects): only drop scope on definitive not-found; clear inactive deleted detail
Address Codex review on #13525:
- ChatRoute: gate scope removal on a 404 (isNotFoundError) or a success that
resolves to a different/empty project, so a transient (non-404) failure under
retry:false no longer unscopes a valid project; keep the chip through transient
errors via retained data.
- useDeleteProjectMutation: also removeQueries({ type: 'inactive' }) so a deleted
project's inactive cached detail is dropped and a later visit refetches into a
not-found state instead of rendering stale cache within cacheTime.
* 🪹 feat: Collapse Empty Projects Section in the Sidebar by Default (#13531)
The Projects section defaulted to expanded, taking sidebar space for users with no
projects. Now derive the default: collapsed when there are no projects and the user
has never toggled the section; expanded once they have a project or explicitly
expand it. Any explicit toggle (new projectsSectionToggled flag) — or a collapse set
before this default existed — is respected.
* 🧭 feat: Scope Model Spec Skills (#13522)
* feat: scope model spec skills
* style: format skill catalog limit
* fix: serialize model spec skill resolution
* test: satisfy model spec load config typing
* fix: apply model spec skills to added conversations
* fix: support alwaysApply frontmatter alias
* fix: address model spec skills review
* 🗂️ feat: Add Deployment Skill Directory (#13523)
* feat: Add deployment skill directory
* chore: Address deployment skill review feedback
* fix: Include deployment skill file metadata
* test: Add deployment skills e2e smoke test
* 🧭 fix: Restore Empty Skill Allowlist Catalog (#13526)
* 🪦 fix: Add Durable MCP Config Tombstones (#13534)
* fix: add durable MCP config tombstones
* fix: preserve scoped config tombstones
* fix: clean up config tombstone lint
* fix: handle empty model spec skill allowlist
* fix: preserve inactive config tombstones
* 🔐 fix: Handle Multiple Concurrent MCP OAuth Login Prompts (#13200)
* fix: handle multiple MCP OAuth prompts
* fix: address MCP OAuth review feedback
* fix: address MCP OAuth prompt lifecycle review
* fix: narrow OAuth prompt slot cleanup
* fix: format OAuth prompt test
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🔐 fix: Reuse MCP OAuth Authorization URL (#13532)
* fix: reuse MCP OAuth authorization URL
* fix: validate MCP OAuth initiate flow ID
* 🪪 fix: Filter ACL Principal Details (#13524)
* fix: filter ACL principal details
* test: type ACL permission pipeline assertions
* test: add ACL permissions e2e coverage
* 🖼️ fix: Support Known Endpoint Group Icons (#13542)
* fix: Support known endpoint group icons
* fix: Resolve known endpoint group icon edge cases
* 📊 fix: Contain Markdown Table Overflow (#13543)
* fix: Contain Markdown table overflow
* fix: Improve Markdown table scrollbar
* 📎 fix: Preserve Provider Document Uploads (#13550)
* fix: Preserve provider document uploads
* test: Add provider upload e2e coverage
* 🔧 chore: Update ESLint config, Import Sorting script, Test Sharding, Bump `@librechat/agents` (#13552)
* 🔧 chore: Update ESLint config, add import sorting script, Test Sharding, Bump `@librechat/agents`
* Change 'no-nested-ternary' rule from 'warn' to 'error' in ESLint config
* Add new scripts for sorting imports in the project
* Update lint-staged configuration to include import sorting
* Modify GitHub Actions workflows to support sharding for unit tests
* chore: remove nested ternary expressions
* refactor: Extract scale multiplier logic into a separate function in CircleRender component
* refactor: Simplify auto-refill rendering logic in Balance component for better readability
* refactor: Improve width style handling in DataTable components for clarity and maintainability
* chore: remove CircleRender component
* delete: Remove CircleRender component as it is no longer needed in the project
* chore: Bump @librechat/agents to version 3.2.31 and update Node.js engine requirement
* Update @librechat/agents dependency from 3.2.2 to 3.2.31 in package-lock.json, api/package.json, and packages/api/package.json
* Change Node.js engine requirement from >=20.0.0 to >=24.0.0 in @librechat/agents
* chore: Add import sorting check to ESLint CI workflow
* Implement a new job in the GitHub Actions workflow to verify import ordering on changed files.
* The job checks for changes in specific file types and reports any import order drift, providing instructions for local fixes.
* 🛠️ fix: Enable Gemini Mixed Tool Config (#13538)
* fix: enable Gemini mixed tool config
* fix: apply Gemini mixed tool flag after skills
* style: match initialize formatting
* style: wrap final tool check
* fix: Respect Vertex auth mode
* style: Sort Agent Initialize Imports
* fix: Tighten Gemini Mixed Tool Gate
* 🏪 feat: Surface Agent Marketplace in Model Selector (#13553)
* feat: surface marketplace in model selector
* chore: sort marketplace selector imports
* fix: localize marketplace selector search
* 🌱 feat: Support Soft Default Model Spec (#13554)
* feat: add soft default model spec
* chore: sort ChatRoute imports
* 🚦 fix: Guard Auth Continuation with Dedicated Limiter (#13555)
* fix: refine auth continuation handling
* test: align auth route mock setup
* fix: separate auth continuation throttling
* test: format auth route mock
* fix: preserve continuation limiter context
* fix: hydrate continuation user before bans
* 🏷️ fix: Categorize Auth Tokens by Flow Type (#13556)
* fix: Scope auth token lifecycle
* fix: Preserve legacy auth token lookup
* fix: Scope verification token cleanup
* 📎 fix: Scope Attachment Usage to Request Owner (#13557)
* fix: harden attachment usage handling
* fix: sort file method imports
* fix: clarify file usage scope
* 🛂 fix: Normalize Verification Flow Error Responses (#13558)
* fix: normalize verification flow responses
* fix: keep verification responses consistent
* 🔀 fix: Reconcile Agent Action Credential Merges (#13559)
* fix: Refine Agent Action Updates
* fix: Format Action Update Helper
* fix: Refine Agent Action Update Handling
* fix: Move Agent Action Update Planning
* fix: Sort Action Update Imports
* chore: Reorder imports in actions.js for clarity
* ⏳ feat: Make OpenID Token Reuse Window Configurable (#13546)
* feat: make OpenID token reuse window configurable via OPENID_REUSE_MAX_SESSION_AGE_MS
The OpenID session-token reuse window in AuthController was a hardcoded 15-minute
constant, forcing /api/auth/refresh to perform a real refreshTokenGrant against the
IdP every 15 minutes even when the current access token is still valid. IdPs that
rotate and revoke the previous access token on refresh then invalidate a token that
is still in use by downstream consumers of the reused OpenID token (e.g. MCP servers
that receive {{LIBRECHAT_OPENID_TOKEN}} and introspect the bearer), producing
~15-minute 401 cycles regardless of the access token's actual lifetime.
Read the window from process.env.OPENID_REUSE_MAX_SESSION_AGE_MS via the existing
math() helper, so it accepts an arithmetic expression like SESSION_EXPIRY (e.g.
60 * 60 * 24 * 1000), defaulting to the existing 15 minutes so behavior is unchanged
unless explicitly configured. The existing 30s-before-expiry guard still forces a
refresh before genuine expiry, so a larger window remains safe.
* fix: extend OpenID reuse session lifetime
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🃏 refactor: Agent Avatar Conversation Icons and Streaming Indicators (#13563)
* 👷 ci: Type-check the Client Workspace (#13560)
The `client/` workspace was never type-checked: the existing typecheck
job only covered `packages/` and `api/`, and Vite/esbuild transpiles
without type-checking, so type errors shipped through every CI gate.
- Add a `typecheck` job to frontend-review.yml running `tsc --noEmit`
over `client/` (zero tolerance), reusing the data-provider +
client-package build artifacts. Triggers on `client/**`,
`packages/client/**`, `packages/data-provider/**`.
- Fix all 168 pre-existing client type errors this surfaced (source +
tests), including genuine latent bugs:
- `getFileConfig()` was typed as merged `FileConfig`, but the server
returns the raw config that `mergeFileConfig()` consumes (`TFileConfig`).
- SidePanel/Agents `Retrieval`/`ImageVision` were bound to `AgentForm`
but use the assistants `Capabilities` enum → `AssistantForm`.
- `useSearchResultsByTurn` read a `sources` field its type lacked.
- Removed orphaned dead code: `Artifacts/Mermaid.tsx` (imported a
never-installed dep) and dead barrel re-exports (`./Plugins`, `./MCPAuth`).
- Narrow `client/tsconfig.json` to the client app (drop `../e2e` and
`../config/translations`, which reference backend/tooling modules) so
the gate's scope matches its trigger.
No `any`/`@ts-ignore`/`as unknown as`. Localized newly-surfaced strings.
* 🧼 fix: Prevent Shared Link Caching and Strengthen Log Redaction (#13561)
* fix: tighten share caching and log redaction
* fix: sort changed imports
* fix: redact splat log arguments
* fix: avoid mutating log metadata during redaction
* fix: redact error and api_key log values
* fix: preserve error log context during redaction
* fix: cover remaining log redaction paths
* fix: bound log redaction work
* fix: align redaction scan cap with log config
* 📦 chore: Update Turbo to v2.9.16 (#13564)
* 🩹 chore: Double-Assert Partial Test Fixture in EndpointIcon (#13567)
The `endpointsConfig` fixture in `EndpointIcon.test.tsx` casts an object whose
values are `{}` to `TEndpointsConfig` (`Record<EModelEndpoint | string, TConfig | null | undefined>`).
`TConfig.order` is required, so `{}` doesn't overlap `TConfig` and the direct
assertion is a TS2352 error under a fresh `tsc --noEmit` over the client
workspace (the type-check job added in #13560), when `librechat-data-provider`
is built from source (the test was added in #13563):
Conversion of type '{ agents: {}; google: {}; }' to type 'TEndpointsConfig'
may be a mistake because neither type sufficiently overlaps with the other.
Give the fixture entries the required `order` field so they're valid `TConfig`
values. This keeps the plain `as TEndpointsConfig` assertion type-checking the
fixture shape, rather than blanking it out with `as unknown as`.
* 🩹 fix: Bump GitNexus to 1.6.5 and Fail-Soft the PR Index Job (#13569)
* 🩹 fix: Bump GitNexus to 1.6.5 and Fail-Soft the PR Index Job
The GitNexus Index workflow began failing on most PRs with
"Analysis failed: Maximum call stack size exceeded". Root cause is in
the pinned gitnexus@1.5.3 CLI: pipeline.js does
`deferredWorkerCalls.push(...chunkWorkerData.calls)`, and once a chunk
yields more extracted calls than V8's argument-count limit (~125k on
this repo) the spread-push throws a RangeError. It is deterministic on
repo size, not flaky — LibreChat simply grew past the threshold, so it
fails "more often" as more branches cross it. Stack-size flags don't
help; it's an arg-count limit, not stack depth.
gitnexus@1.6.5 refactored that code path (the .calls spread-pushes are
gone) and indexes this repo cleanly. Bump the indexer, the deploy image
tag/build-arg, and the Dockerfile default in lockstep (an index written
by 1.6.5 must be served by a 1.6.5 server), and move the co-pinned
@ladybugdb/core to 0.16.1 to match.
Also make the index job fail-soft on pull_request events so a future
tool-internal crash degrades gracefully instead of red-X'ing PRs. Push,
dispatch, and /gitnexus command runs still fail loudly, keeping the
deploy-gating and completion-comment logic correct.
* 🐳 fix: Unbreak the GitNexus Deploy Image for 1.6.5
Addresses two issues in the deploy image surfaced after the 1.6.5 bump:
- The image build's lbug-adapter patch grepped
dist/mcp/core/lbug-adapter.js for "LOAD EXTENSION fts", but in 1.6.5
that file is a shim re-export and the FTS load moved to
dist/core/lbug/lbug-adapter.js. The grep would fail the build on the
next image rebuild. The patch is also obsolete: 1.6.5 loads the vector
extension itself via loadVectorExtension. Removed the patch step.
- The image installed only gitnexus, letting @ladybugdb/core resolve
freely via gitnexus's ^0.16.1 range while the index workflow pins
0.16.1 exactly. Pin the native DB in the image too (nested under
gitnexus so install-extensions.js keeps resolving it), restoring the
intended indexer/server lockstep.
* 🏷️ fix: Preserve Generated Conversation Title on Stop (#13568)
Immediate title generation discarded an already-generated title when the
user stopped the turn, both in the backend (skipped saveConvo) and the
frontend (rolled back the streamed title), leaving the chat as "Untitled"
in the interim and "New Chat" after refresh.
Split the title abort into two signals: `signal` still cancels an in-flight
title model call on Stop, while a new `discardSignal` discards an
already-generated title only when the stream is superseded by a newer run
or the turn fails. A plain user Stop now persists and keeps the title.
The frontend no longer rolls back a real, already-applied title on an
aborted final event.
* 🌲 test: Add E2E Coverage for Message Tree Streaming (#13570)
* add e2e message tree stream coverage
* fix e2e message tree review findings
* expand message tree e2e recovery coverage
* fix stream-start failure recovery coverage
* 🏛️ refactor: Prioritize Deployment Skills over Persisted Duplicates (#13575)
* fix: prefer deployment skills on name collision
* chore: sort deployment skill imports
* fix: dedupe deployment collision warnings
* fix: return logger from warning spy
* fix: preserve skill collision pagination
* fix: honor db page boundary for skill merges
* 📻 fix: Replay MCP OAuth Prompts for Coalesced Connections (#13565)
* fix: Replay MCP OAuth URL for Joined Connections
* chore: Sort MCP OAuth Imports
* test: Restore MCP OAuth Registry Spies
* fix: Replay pending MCP OAuth prompts
* fix: Replay MCP OAuth on Stream Resume
* fix: Preserve MCP OAuth Replay Context
* chore: Format MCP OAuth Replay Context
* test: Expect MCP OAuth Replay Expiry
* fix: Render pending MCP OAuth prompts
* chore: Clean MCP OAuth Replay Type Narrowing
* fix: Stabilize new MCP OAuth chats
* fix: Re-emit cached MCP OAuth prompts
* fix: Replay pending OAuth for selected MCP tools
* fix: Avoid stalling pending MCP OAuth replay
* test: Clean MCP OAuth review findings
* test: Restore MCP OAuth registry spy
* fix: Resolve OAuth Typecheck Regressions
* fix: Harden MCP OAuth replay edge cases
* test: Cover MCP OAuth joined prompt expiry
* test: Mark joined OAuth replay fixture
* test: Use OAuth fixture for joined replay expiry
* fix: Anchor resumed MCP OAuth prompts
* fix: Seed resumable turn metadata before MCP init
* test: Format resume metadata regression
* fix: Prioritize resumable stream routes
* fix: Preserve MCP OAuth resume message tree
* test: Fix MCP OAuth Resume Test Types
* fix: Replay MCP OAuth Regenerate Prompts
* fix: Skip OAuth-only Abort Persistence
* fix: Stabilize OAuth Resume Replay
* fix: Target Non-Tail Regenerate Responses
* fix: Scope Regenerate Step Updates
* fix: Clean Up OAuth Abort State
* fix: Preserve Regenerate Branch Siblings
* fix: Preserve OAuth Resume Branch State
* fix: Preserve OAuth Branch Resume State
* chore: Sort OAuth Resume Imports
* fix: Address OAuth Resume Review Findings
* test: Fix Abort Fixture Typing
* 📊 feat: Surface Message Feedback as Langfuse Scores (#13544)
* feat: surface message feedback (thumbs up/down) as Langfuse scores
When Langfuse tracing is enabled, the message feedback endpoint now posts a
boolean `user-feedback` score (1/0 + tag/comment) to Langfuse for the
assistant message's trace; clearing feedback deletes the score. Fire-and-
forget, so the feedback UX never blocks on Langfuse.
Linking is lookup-free: the run opts into deterministic Langfuse trace ids
(`langfuse.deterministicTraceId`, passed to the agents Run), so the trace id
is sha256(messageId)[:32]. The feedback route recomputes the same id and
scores by it.
- api/server/services/Langfuse.js: POST/DELETE /api/public/scores (env-gated)
- api/server/utils/langfuseTrace.js: traceIdForMessage(messageId)
- api/server/routes/messages.js: fire feedback score after the Mongo write
- packages/api: pass langfuse.deterministicTraceId to the run
- bump @librechat/agents to ^3.2.21 (adds LangfuseConfig.deterministicTraceId)
Closes #13537
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: match Langfuse trace environment for feedback scores
@librechat/agents passes no environment to its Langfuse tracer, so
@langfuse/otel falls back to LANGFUSE_TRACING_ENVIRONMENT and otherwise to
Langfuse's "default". The score helper instead fell back to NODE_ENV, so a
deployment with only NODE_ENV=production filed scores under "production" while
the trace stayed on "default" — the score never landed on the trace.
Use LANGFUSE_TRACING_ENVIRONMENT only, and omit `environment` when unset so
Langfuse defaults both score and trace to "default".
Addresses Codex review on #13544.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: don't require LANGFUSE_BASE_URL to post feedback scores
The agent tracer emits traces with just the public/secret keys (defaulting to
Langfuse Cloud, or via the legacy LANGFUSE_BASEURL alias), but the score helper
disabled itself unless LANGFUSE_BASE_URL was set — so an otherwise-traced
deployment silently posted no scores. Resolve the base URL the same way the
tracer does (LANGFUSE_BASE_URL -> LANGFUSE_BASEURL -> Cloud) and gate enablement
on the credentials only.
Addresses Codex review on #13544.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: only post feedback scores for agent-endpoint messages
The feedback route is shared by all message types, but deterministic Langfuse
trace IDs are only enabled for agent runs. Rating a message from a non-agent
endpoint (with Langfuse configured) posted a user-feedback score for
sha256(messageId) that no trace will ever match, leaving orphan scores.
Gate scoring on isAgentsEndpoint(message.endpoint); `updateMessage` now returns
`endpoint` so the route can check it.
Addresses Codex review on #13544.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: gate feedback scoring by !isAssistantsEndpoint, not isAgentsEndpoint
The previous gate used isAgentsEndpoint, which only matches the literal
`agents` endpoint. But provider endpoints (anthropic, openai, custom, …) run
through the agents runtime as ephemeral agents and DO emit deterministic
AgentRun traces, so isAgentsEndpoint('anthropic') === false suppressed scoring
for the common case. Only the OpenAI/Azure Assistants endpoints use a separate
runtime with no agent trace, so gate on !isAssistantsEndpoint instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style: sort message method imports
* fix: honor Langfuse tracing gates for feedback scores
* refactor: move Langfuse feedback logic to api package
* fix: support Langfuse host for feedback scores
* test: type Langfuse feedback fetch mock
* chore: compact Langfuse feedback comment
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🧊 perf: Memoize Completed Markdown Blocks During Streaming (#13576)
Render assistant markdown as independently memoized top-level blocks instead of a
single ReactMarkdown that re-parses and re-highlights the entire message on every
streamed token. Once a block's source slice is stable it skips re-parse/re-render;
only the final, still-growing block re-parses.
- splitMarkdown: split a message into top-level blocks via mdast-util-from-markdown
(+ gfm/directive/math extensions) using node source offsets; also report per-block
executable-code and artifact index counts.
- MarkdownBlocks: render each block memoized on its raw slice, each wrapped in its
own CodeBlock/Artifact providers seeded with prefix-summed base indices, so the
document-order indices used to match code-execution results stay stable under
memoization (verified by OLD-vs-NEW parity tests across direct + streamed renders).
- CodeBlockContext/ArtifactContext: add optional baseIndex (default 0, fully
backward compatible) so per-block providers continue the running index.
- markdownConfig: extract the shared remark/rehype plugins + components map.
- deps: declare mdast-util-from-markdown, mdast-util-gfm/math/directive and the
micromark gfm/math/directive extensions as direct client dependencies (previously
resolved transitively via react-markdown).
- Tests: splitter unit tests; index parity + DOM equivalence vs the whole-message
renderer; rendering smoke tests.
- Bench (MarkdownBlocks.bench.tsx, outside __tests__ so the default jest run skips
it): ~88% fewer code-block renders and ~2.3x faster cumulative render across a
simulated stream.
* 🔐 fix: Resolve Env Variables in MCP OAuth URL Fields (#13573)
* fix: resolve env variables in MCP OAuth URL fields before validation
Apply the extractEnvVariable transform to authorization_url, token_url,
redirect_uri, and revocation_endpoint in OAuthOptionsBaseSchema. Without
this, ${ENV_VAR} syntax in these fields caused a Zod URL validation error
at startup before any env substitution could happen.
The same .transform().pipe() pattern is already used on all transport url
fields (SSE, WebSocket, StreamableHTTP) and ProxyUrlSchema.
Closes #13572
* fix: block env var expansion in user OAuth URL fields
Override redirect_uri and revocation_endpoint in UserOAuthOptionsSchema
with userOAuthEndpointUrlSchema, matching the existing overrides for
authorization_url and token_url. Without this, user-submitted configs
could inherit the extractEnvVariable transform added to the base schema
and resolve env vars like ${OPENAI_API_KEY} in those fields.
Add envVarPattern rejection to userOAuthEndpointUrlSchema so that
valid-URL-shaped payloads containing ${VAR} patterns are also blocked,
not just bare non-URL strings. Move envVarPattern declaration above the
schema to make it available at module evaluation time.
Add regression tests for all four OAuth URL fields on the user path,
using structurally valid URLs with embedded ${VAR} patterns to confirm
it is the env var guard — not URL shape — that rejects them.
* ⚡ perf: Migrate `data-schemas` Build to tsdown with isolatedDeclarations (#13578)
* ⚡ perf: Migrate data-schemas Build to tsdown with isolatedDeclarations
Replace Rollup with tsdown (rolldown + oxc) for @librechat/data-schemas. With the source made isolatedDeclarations-clean, oxc emits .d.ts without tsc, dropping the package build from ~5.8s to ~0.8s (~7x).
- Annotate exported model/method factories for isolatedDeclarations (TypeScript's fixMissingTypeAnnotationOnExports codefix plus hand-authored interfaces); type the ~44 mongoose `any`s and add an explicit PromptMethods interface (previously its declaration was silently dropped by the Rollup build).
- Repoint package.json exports/main/module/types to tsdown output; drop rollup config.
- Config lives in tsdown.config.mjs (native ESM) so CI without a TS-config loader can build it; bundle `dotenv` so the package stays self-contained for its env-loading side effect.
- Fix a latent token `metadata` mismatch the accurate types surfaced: widen TokenCreate/UpdateData inputs to accept plain objects, flatten OAuthMetadata at the api boundary.
- Update mongoMeili/aclEntry specs to the precise model types; drop redundant terser minification from data-provider's library build.
All data-schemas tests pass; api builds clean against the new output.
* 🔧 chore: Hash tsdown.config.mjs in data-schemas CI build-cache keys
The data-schemas build switched from rollup to tsdown, but the build-data-schemas / build-api cache keys in backend-review, config-review, and playwright-mock still hashed the (now-deleted) rollup.config.js. Hash tsdown.config.mjs instead so a config-only change invalidates the cached dist/api builds. (Found by Codex review.)
* 🔧 chore: Replace deprecated tsdown `external` with `deps.neverBundle`
tsdown 0.22 deprecated the top-level `external` option in favor of `deps.neverBundle`. Migrate the data-schemas config and set `deps.onlyBundle: false` to silence the (intentional) dotenv bundling hint. Build output and externalization are unchanged — dotenv bundled, all peers external.
* ⬆️ chore: Bump TypeScript to 5.9.3 (+ typescript-eslint 8.60.1) (#13584)
Bumps typescript 5.3.3 -> 5.9.3 across all workspaces. typescript-eslint must move 8.24.0 -> 8.60.1 too: 8.24's typescript peer was capped at <5.8.0; 8.60.1 widens it to <6.1.0.
Two errors surfaced by the newer compiler are fixed:
- api/src/rum/proxy.ts: TS 5.9 made `Buffer` generic (`Buffer<ArrayBufferLike>`), which no longer structurally matches `BodyInit`; cast the fetch body (Node's fetch accepts a Buffer at runtime).
- client usePresetIndexOptions.ts: drop a dead `|| {}` on an object spread (always truthy — flagged by the new TS2872 check).
All four package typecheck jobs + the client app typecheck pass under 5.9.3; builds (tsdown + rollup) and the rum proxy tests are unaffected.
* 🌿 fix: Anchor Post-Auth MCP Stream to Submission Message Tree (#13582)
* fix: Preserve MCP OAuth post-auth message tree
* fix: Hydrate MCP OAuth replay after created event
* fix: Satisfy OAuth replay typecheck
* 🚧 fix: Add Per-User Throttle to 2FA Continuation Attempts (#13583)
* fix: refine auth continuation throttling
* chore: import order
* 📌 fix: Preserve Project Scope Through Enforced Model Specs (#13586)
* 🔧 chore: Enforce isolatedDeclarations in data-schemas tsconfig (#13593)
Enable `isolatedDeclarations` (and `declaration`) in
packages/data-schemas/tsconfig.json so any exported declaration missing
an explicit type annotation is flagged directly in editors and the CI
typecheck, instead of only surfacing during the tsdown/oxc dts emit at
build time.
The package is already fully annotated, so this is a zero-error,
enforcement-only change that keeps data-schemas eligible for the fast
oxc-based declaration emit going forward.
* ⚡️ refactor: Migrate `@librechat/api` build to `tsdown` (#13595)
* ⚡️ refactor: Migrate @librechat/api build to tsdown
Replace Rollup with tsdown (rolldown + oxc isolated-declarations) for the
@librechat/api package build, mirroring the merged data-schemas migration.
- Add tsdown.config.mjs (cjs output, oxc dts, externalize all bare deps,
bundle first-party `~/` + relative imports)
- Annotate exports for isolatedDeclarations (codefix-driven). Collapse the
tokens.ts model->token maps to Record<string, Record<string, number>> and
switch validation.ts's runtime `files` field from z.any() to z.unknown()
so no explicit `any` is introduced
- Repoint package.json main/types/exports to tsdown's .cjs/.d.cts output
- Add src/telemetry.ts entry shim so the two index.ts entries don't collide
in oxc's flat dts output (stable dist/telemetry.{cjs,d.cts})
- Delete rollup.config.js
Build time ~36s -> ~0.5s. No runtime behavior change: 5712 unit tests pass,
both entries load via require(), legacy /api consumes them unchanged.
* 👷 ci: Hash packages/api/tsdown.config.mjs in build-api cache keys
The build-api cache keys hashed `packages/api/server-rollup.config.js`,
which never existed (api used `rollup.config.js`, now removed) — a copy-paste
artifact from the data-provider key that matched no file. Replace it with the
new `packages/api/tsdown.config.mjs` so edits to the build config (entry,
format, externals) bust the api build cache, matching the data-schemas key.
* ⚡ refactor: Migrate `data-provider` Build to `tsdown` (split tsc dts) (#13597)
Replace the Rollup + `rollup-plugin-typescript2` build with a split
pipeline: tsdown (rolldown) bundles the JS in ~0.2s, and plain `tsc`
emits the declarations to `dist/types` (~2s). Full cold build drops from
~9.2s to ~2.5s (~3.6x) with zero source changes.
Unlike data-schemas, the fast oxc/isolated-declarations dts path isn't
viable here: the package's 78 exported zod schemas produce 374
`isolatedDeclarations` errors (TS9013/TS9038) and a `z.ZodType<T>`
annotation would break the 76 downstream `.extend`/`.shape`/`.pick`
usages. Plain `tsc` keeps the rich zod types intact, and since dts was
never the bottleneck (rollup-plugin-typescript2 was), the win stands.
- dts stays unbundled in `dist/types/` — identical to the prior output,
so the existing deep `dist/types` imports and the exports `types`
paths are unchanged.
- ESM output renamed `index.es.js` -> `index.mjs` (via the exports map;
no consumer hardcodes the old path). cjs/types paths unchanged.
- `./react-query` now emits a real cjs build + types — the exports map
already promised them, but Rollup only ever built the esm file.
- Kept `rollup` + the plugins used by `server-rollup.config.js`
(the `rollup:api` server-bundle smoke test in backend-review.yml);
removed only the deps used solely by the deleted `rollup.config.js`.
- Repointed CI build-cache keys from `rollup.config.js` to
`tsdown.config.mjs`.
* ⚡ refactor: Migrate `@librechat/client` build to `tsdown` (#13596)
* ⚡ refactor: Migrate @librechat/client build from Rollup to tsdown
Mirrors the data-schemas migration. Replaces Rollup (rpt2 + postcss) with
tsdown (rolldown + oxc); the package build drops from tens of seconds to ~0.3s.
- Emit isolated-declaration .d.ts via oxc (dts.oxc) and enforce
isolatedDeclarations in tsconfig for editor DX (source made clean: explicit
export type annotations added across src, no `any`).
- Extract component CSS to dist/style.css so the CJS output stays valid
CommonJS (the prior postcss runtime-injection produced an ESM import in the
CJS bundle that breaks jest/require). Imported once in the client app entry;
Vite bundles it for the app.
- Repoint package.json to dual .mjs/.cjs + .d.mts/.d.cts and add ./style.css
and ./package.json exports.
- Update CI build-cache keys to hash tsdown.config.mjs; remove rollup.config.js.
* 🔧 chore: address Codex review on client tsdown migration
- Add tsdown.config.mjs to turbo.json build `inputs` so changes to the new
bundler config invalidate the Turbo cache (the shared inputs only listed the
rollup configs). Also covers the already-migrated data-schemas.
- Name the memoized default export (ControlComboboxMemo) instead of the
codefix-generated `_default_1`, for clearer stack traces / grepping.
* 🧩 feat: Enable Model Spec Subagents (#13598)
* 📦 chore: Declare runtime deps externalized by tsdown in `@librechat/api` (#13600)
The tsdown migration (#13595) externalizes all third-party imports
(Rollup inlined them), so several modules the api source imports must be
present at runtime. Six were not, causing production (`npm ci --omit=dev`)
to crash on boot with `Cannot find module 'get-stream'` (then the next).
Fixed following the package's existing convention — packages/api declares
runtime libs as `peerDependencies`, and the `/api` app provides them as
real `dependencies` (how express/mongoose/sharp already resolve):
- `api/package.json` (the prod app, the provider): add the 3 that were
missing — `get-stream`, `jszip`, `mongodb`. (`dedent`/`lodash`/`nanoid`
were already provided by /api.)
- `packages/api/package.json`: add all 6 to `peerDependencies` (the
contract) and to `devDependencies` (workspace build/tests), matching
the existing `mammoth`/`pdfjs-dist`/`sanitize-html` dev+peer pattern.
`jszip`/`mongodb` move out of dev-only (were pruned in production).
Pinned to CJS-compatible majors (get-stream@6, nanoid@3). Verified the
built bundle has zero undeclared externals and the 3 newly-provided deps
are production (non-dev) in the lockfile, so they survive `--omit=dev`.
* 📋 refactor: Attach Message Context to Langfuse Feedback Scores (#13604)
* 🪞 fix: Preserve Model Spec Icons Across Stream Resume and Abort (#13603)
* 👷 ci: Add API runtime smoke (boot the production image) to docker-smoke (#13605)
* 👷 ci: Add API runtime smoke (boot the production image) to docker-smoke
The docker-smoke workflow only built the `client-package-build` stage and
never booted the runtime, so it couldn't catch the class of regression that
recently took production down: the api tsdown bundle externalizes runtime
deps that, after `npm ci --omit=dev`, were missing from the image
(`Cannot find module 'get-stream'`).
- Add an `api-runtime-smoke` job that builds the real production image
(final `api-build` stage, `npm ci --omit=dev`), then:
1. loads the @librechat/api bundle's full require graph in the pruned
image (deterministic, no DB) — fails on any missing/ESM-incompatible
runtime dependency.
2. boots the actual entrypoint and asserts no module-load crash (the
server loads its require graph before connecting to Mongo, so this
surfaces without a database).
- Expand triggers to include `packages/api/**`, `packages/data-schemas/**`,
and `api/package.json` (previously a packages/api change only triggered
this via a root lockfile change, and even then only built the client stage).
- Add gha build cache + concurrency cancellation to bound CI cost.
* 👷 ci: Address Codex review — boot smoke against real Mongo + crash detection
- Boot the production image against a real MongoDB container with the env
the server needs, so the *entire* require graph loads. `api/db/connect.js`
throws at module scope without `MONGO_URI` and is imported before
models/services/routes, so the previous no-env boot exercised almost none
of the legacy API graph. (Codex finding 2)
- Gate on `/health` returning 200 AND the container staying alive, failing on
any container exit. A non-module startup crash (ReferenceError, SyntaxError,
bad config) now fails the smoke instead of slipping past a missing-module
grep. (Codex finding 3)
- Expand trigger from `api/package.json` to `api/**`, since the image copies
the whole `api/` tree and runs `node server/index.js`. (Codex finding 1)
* 👷 ci: Address Codex round 2 — poll /readyz + cover all image inputs
- Poll /readyz instead of /health. /health returns 200 at app.listen, but
initializeMCPs() and checkMigrations() run *after* listen and process.exit(1)
on failure; /readyz only returns 200 once serverReady is set after those
complete. So post-listen startup crashes now fail the smoke too. (finding A)
- Expand triggers to every source tree copied into the production image:
client/**, config/**, skill/** (the final stage copies client/dist, config,
and skill). (finding B)
* 👷 fix: Preserve disabled build info flag in loadDefaultInterface (#13608)
* 🛬 fix: Coalesce Auth Recovery into a Single Refresh Flight (#13618)
* fix auth recovery singleflight
* add auth recovery e2e coverage
* handle invalid auth redirect timestamp
* 🧵 fix: Reject Preliminary Parent Follow-Ups (#13619)
* fix: Reject preliminary parent follow-ups
* chore: Sort frontend imports
* fix: Narrow preliminary parent detection
* fix: Preserve refused submit state
* fix: Propagate refused submit result
* 🚫 fix: Hide Empty Agents Endpoint from Model Selector (#13624)
* fix: prevent empty agents endpoint selection
* fix: sort endpoint item imports
* ♊ chore: Upgrade `@google/genai` SDK to ^2.8.0 (#13625)
Keep the Google Gen AI SDK aligned with the latest 2.x release. Updates the
declared range in both backend manifests (api, packages/api) and regenerates
the lockfile to resolve @google/genai to 2.8.0.
No application code changes: the sole consumer
(api/app/clients/tools/structured/GeminiImageGen.js) uses the stable
`GoogleGenAI` constructor and `models.generateContent` API, and the upstream
changelog records no breaking changes to those between 2.0 and 2.8.
Closes #13551
* ♊ fix: Sanitize MCP Tool Schemas for Gemini/Vertex Compatibility (#13623)
* 🧰 fix: Flatten union schemas for Gemini/Vertex MCP tool compatibility
`@langchain/google-common`'s `zod_to_gemini_parameters` throws "Gemini cannot
handle union types" on any genuine `anyOf`/`oneOf` (e.g. discriminated unions),
so MCP tools shipping union-typed schemas crash on the Google endpoint while
working fine on OpenAI/Claude.
Add `flattenJsonSchemaUnions` (packages/api) to collapse unions to their first
non-null member and multi-entry `type` arrays to a single nullable type, and
apply it in `createToolInstance`'s existing `isGoogle` branch so only the
Google/Vertex path is affected. Lossy by design, mirroring the existing
empty-object fallback.
Closes #13612
* 🩹 fix: Address Codex review — preserve fields, strip null enums, cover definitions path
- Preserve parent-level `properties`/`required` when collapsing a union: merge the
chosen branch into the parent instead of overwriting, so args declared outside the
union (e.g. always-required fields) still reach Gemini.
- Drop the `null` member from `enum` when a union/type-array makes a field nullable,
keeping Gemini's required homogeneous-enum invariant.
- Propagate the Google-flattened schema to the definitions/deferred-tool path:
thread `provider` into `loadToolDefinitions` and flatten there, and store the
flattened schema on `mcpJsonSchema` so `extractMCPToolDefinition` no longer emits
raw unions on Google/Vertex.
* 🎨 style: Sort imports in tools/definitions per import-order check
* ♊ feat: Broaden union flatten into a full Gemini schema sanitizer
The union flatten alone wasn't enough — real GitHub MCP tools on Gemini also 400
with `Invalid value ... (TYPE_STRING), true`, because Gemini's function-calling
Schema (https://ai.google.dev/api/caching#Schema) accepts only a restricted JSON
Schema subset, and `enum` is `Type.STRING`-only.
Rename `flattenJsonSchemaUnions` → `sanitizeGeminiSchema` and broaden it (one pass,
Gemini-gated) to cover the documented subset:
- Keep only string `enum` values; drop the keyword for non-string types (fixes the
reported boolean-enum 400, incl. boolean `const` normalized to `enum: [true]`).
- `const` → single-value string enum, or drop if non-string.
- Merge `allOf` intersections; fold `exclusiveMinimum`/`exclusiveMaximum` into
`minimum`/`maximum`.
- Strip unsupported keywords: `additionalProperties`, `default`, `$schema`, `$id`.
- (Existing) collapse `anyOf`/`oneOf`, multi-entry `type` arrays, nullable.
Grounded in Google's Schema docs rather than reverse-engineered from 400s. Verified
end-to-end against the real `@langchain/google-common` converter. Complements
danny-avila/agents#232 (langchain bump), which defers schema flattening to LibreChat.
* 🩹 fix: Gate enum retention on the effective (collapsed) type
Codex review: a mixed-type enum like `type: ['integer','string'], enum: [1,'auto']`
collapsed the type to `integer` but still kept the string value `'auto'`, yielding
`{type:'integer', enum:['auto']}` — a non-string type with an enum, which Gemini
rejects. Keep `enum` only when the effective collapsed type is string (or unset),
and stamp `type: 'string'` on a surviving typeless enum (e.g. a string `const`
discriminator) so it satisfies Gemini's Type.STRING enum requirement.
* 🌍 fix: Prefer Imagen-Specific Location over Default Google Region (#13613)
* 🧠 fix: Bound Memory Agent Input (#13606)
* 📖 feat: Add Claude Fable 5 Support (#13628)
* 📖 feat: Add Claude Fable 5 Support
Claude Fable 5 (`claude-fable-5`) is Anthropic's most capable widely
released model (GA 2026-06-09). Its naming drops the opus/sonnet/haiku
tier, so LibreChat's name-parsing helpers miss it; this teaches them the
Mythos-class family (Fable / Mythos) and registers the model.
- Add `parseMythosClassVersion` and route Fable/Mythos through
`supportsAdaptiveThinking`, `omitsThinkingByDefault`,
`omitsSamplingParameters`, and `supportsContext1m`
- Extend the Bedrock detection regexes (beta headers + adaptive-thinking
branch) and `checkPromptCacheSupport` to match `claude-(fable|mythos)`
- Return 128K max output for Fable/Mythos in `maxOutputTokens.reset`/`set`
- Register `claude-fable-5` in shared Anthropic + Bedrock model lists,
1M context / 128K output token maps, and $10/$50 pricing with 12.5/1
cache rates (`claude-mythos-5` added to token + pricing maps only,
since it is limited-availability)
- Update `.env.example` and the Vertex `librechat.example.yaml` examples
- Add parallel tests across tokens, Anthropic llm config, the Bedrock
parser, and tx pricing
* 🧹 refactor: Centralize Mythos-class detection; address review feedback
- Add `isMythosClassModel` + `MYTHOS_CLASS_FAMILIES` in schemas.ts as the
single source of truth for the Fable/Mythos family; route every gate
(adaptive thinking, omit-thinking, omit-sampling, 1M context, prompt cache,
128K max-output reset/set) through it. A future sibling class is now a
one-line edit.
- [Codex P2] Exclude Mythos-class from getBedrockAnthropicBetaHeaders: Fable/
Mythos ship 128K output + fine-grained tool streaming by default, and the
legacy output-128k-2025-02-19 beta is 3.7-Sonnet-only on Bedrock and risks
request rejection. They still get adaptive thinking + effort.
- [Copilot] Add Mythos 5 test parity (name variations, cache rates, pinned
$10/$50) in tx.spec; add Mythos context/max-output/name-match in tokens.spec;
fix the stale claude-3-7-sonnet-only comment in bedrock.ts.
- Add isMythosClassModel unit tests covering all declared families.
* 📝 docs: Clarify Mythos-class Bedrock requirements; correct beta-omit rationale
Verified live against Bedrock (acct 951834775723, us-west-2):
- anthropic.claude-fable-5 IS a real Bedrock catalog model, INFERENCE_PROFILE-only
exactly like the existing anthropic.claude-opus-4-7/4-8 and claude-sonnet-4-6
default entries (refutes the "invalid model id" review claim).
- Mythos-class also requires opting into Anthropic data sharing (Bedrock Data
Retention API) before invocation.
Changes:
- .env.example: note that Mythos-class (Fable/Mythos) is inference-profile-only on
Bedrock and needs the data-sharing opt-in.
- bedrock.ts: reword the beta-omit comment to the verified rationale — output-128k /
fine-grained-tool-streaming are built-in/no-op for the 4.7+ generation, so omitting
them is lossless (dropped the unverified "Bedrock may reject" wording).
* 🔄 refactor: Reorganize imports in schemas.ts and tx.spec.ts
- Moved `TFeedback` and `Tools` imports to the top of `schemas.ts` for better readability.
- Adjusted import order in `tx.spec.ts` to maintain consistency and improve clarity.
* 🎙️ fix: Resolve Speech Recognition CJS/ESM Import Shape (#13631)
* 🕳️ fix: Guard Sparse Content Parts in Message Nav Preview (#13632)
* 🔇 fix: Suppress Expected Speech Synthesis Cancellation Errors (#13627)
* 📦 chore: Bump `@librechat/agents` to v3.2.32 (#13633)
* ✂️ fix: Deduplicate Skill Bodies Across Fresh Primes and History (#13610)
When a skill is primed fresh this turn (manual $-popover or always-apply) AND
also appears in history as a `skill` tool_call, its SKILL.md body was injected
twice — once by injectSkillPrimes and once reconstructed by formatAgentMessages.
- add `collectFreshSkillPrimeNames` helper (packages/api) — union of manual +
always-apply prime names
- client.js: pass the set as `skipSkillBodyNames` to formatAgentMessages for
both the initialMessages and memoryMessages paths so the body reconstructs
once. Names not primed this turn still reconstruct (sticky manual re-prime).
Requires `@librechat/agents` with `skipSkillBodyNames` support; the published
dist silently ignores the unknown option until upgraded.
* 🎞️ fix: Stream File Authoring Previews from Partial Tool Args (#13634)
* 🌊 fix: Stream File Authoring Previews from Partial Tool Args
* 🧹 chore: Sort Imports in FileAuthoringCall
* 👁️ feat: Keep File Authoring Input Visible After Completion
* ⏳ fix: Extend and Decouple MCP OAuth Flow Timeouts (#13622)
* ⏳ fix: Extend and decouple MCP OAuth flow timeouts
The OAuth auth button disappeared after 2 minutes (the internal OAuth
handling timeout) while the flow state lived for 3 minutes, leaving users
who didn't click immediately stuck in an unrecoverable re-auth loop. The
handling timeouts also reused the connection/init timeout, so a short
initTimeout would shrink the OAuth window further.
- Add MCP_OAUTH_HANDLING_TIMEOUT (10m) and MCP_OAUTH_FLOW_TTL (15m) to mcpConfig
- Decouple the reactive/proactive OAuth waits from initTimeout/connectionTimeout
- Use OAUTH_FLOW_TTL for the FlowStateManager TTL and the UI status window
- Ensure the flow TTL outlives the handling timeout, fixing the
"Flow state not found" race
- Remove dead FLOW_TTL constant and document new env vars
Fixes #13615
* ⏳ fix: Coordinate OAuth pending window with handling timeout
Address Codex review: the extended OAuth wait was still capped by other
timeouts that were not updated.
- Align PENDING_STALE_MS (button validity + pending-flow reuse window)
with MCP_OAUTH_HANDLING_TIMEOUT so a flow stays reusable for the full
wait instead of 2 minutes (Finding 3)
- Clamp MCP_OAUTH_FLOW_TTL to never fall below the handling timeout so a
callback near the deadline still finds its flow state (Finding 2)
- Floor attemptToConnect's timeout to the handling window for OAuth
servers so the reactive in-connect OAuth wait is not killed by the
30s connection timeout (Finding 1)
- Update flow staleness tests to reference the threshold symbolically
* ⏳ fix: Align OAuth window across status, action flows, and client polling
Address Codex round 2: extending the server wait exposed three more
windows that were still capped or now over-extended.
- checkOAuthFlowStatus reports a PENDING flow as active only within the
usable PENDING_STALE_MS window, not the longer Keyv retention TTL, so
the connect button reappears instead of a stuck 'connecting' state
- Give Action (custom tool) OAuth its own FlowStateManager on the prior
3-minute TTL so the longer MCP OAuth TTL can't leave an action tool
call waiting up to 15 minutes
- Extend the MCP server-card client polling to the 10-minute handling
window so a user who completes OAuth after 3 minutes is still picked up
* 🧪 test: Make stale-flow CSRF test track PENDING_STALE_MS
The CSRF-fallback stale-flow test hardcoded a 3-minute age, which is now
within the 10-minute PENDING_STALE_MS window and was wrongly treated as
active. Derive the age from PENDING_STALE_MS so it tracks the constant.
* ⏳ fix: Add grace buffers and surface OAuth timeout to the client
Address Codex round 3 (near-deadline edges):
- Clamp MCP_OAUTH_FLOW_TTL to handling timeout + 60s grace (not equality),
so flow state outlives the wait instead of expiring at the same instant
- Extend attemptToConnect's OAuth floor by a 60s grace so a user who
authorizes near the deadline still gets the post-OAuth reconnect
- Surface OAUTH_HANDLING_TIMEOUT on the connection-status response and
have the client poll for the configured window instead of a hardcoded
10 minutes, so a tuned server deadline isn't capped on the client
* ⏳ fix: Refresh client OAuth timeout from the first status refetch
If the connection-status cache is empty when polling starts, the client
captured the 10-minute fallback and never picked up a tuned oauthTimeout.
Re-read it after each refetch so a longer configured deadline is honored
even on a cold cache.
* 📝 refactor: Type oauthTimeout on MCPConnectionStatusResponse
Declare the oauthTimeout field on the shared response type in
data-provider instead of an ad-hoc inline cast in the client hook, and
replace the pre-existing 'as any' on the status query read with the
typed getQueryData. Type-level only; no runtime change.
* 🗝️ fix: Resolve MCP Runtime User and Request Placeholders (#13626)
* fix: Resolve MCP Runtime User Placeholders
* fix: Harden MCP Runtime Placeholder Connections
* fix: Update MCP Source Tag Test Expectations
* fix: Complete MCP Runtime Placeholder Reinit
* fix: Harden MCP Request Scoped Runtime Configs
* fix: Align MCP OAuth Tests With Domain Policy
* fix: Harden MCP Runtime Resolution Edges
* fix: Avoid MCP Runtime Reprocessing Pitfalls
* fix: Reuse MCP Request Scoped Tool Discovery
* fix: Validate MCP Body Runtime Fields
* 🛡️ refactor: Harden runtime placeholder edges from review
- Warn at inspection when a trusted server URL contains runtime
placeholders but no domain allowlist restricts the resolved target
- Document the three resolution sites that must stay in sync so the
validated config always matches the connected one
- Note the per-call connect cost of ephemeral GRAPH/BODY connections
- Drop the no-op removeUserConnection in callTool's ephemeral cleanup;
ephemeral connections are never stored, and removing the entry could
orphan a still-connected cached connection after a config change
* 🪪 fix: Cover oauth_headers, Graph URL gating, and request-scoped reconnects
Address Codex review:
- Resolve runtime placeholders in oauth_headers (processMCPEnv + Graph
pre-pass) and include the field in placeholder detection, so OAuth
discovery/token requests no longer send literals; consolidate the
detection field lists into one helper
- Defer the early domain gate when the URL still carries a Graph
placeholder (resolved async later); the authoritative
assertResolvedRuntimeConfigAllowed check still enforces policy
- Bypass the 10s reconnect throttle for request-scoped servers, which
re-fetch tool definitions on every message by design
* 📥 fix: ChatGPT Conversation Import Failures (#13637)
* 🛰️ feat: Add GPT-5.5 + Frontier OpenAI Models, Drop Deprecated Defaults (#13636)
* 🛰️ feat: Add GPT-5.5 + Frontier OpenAI Models, Drop Deprecated Defaults
* 🛰️ fix: Address Codex Review on OpenAI Model Refresh
- Replace nonexistent gpt-5.5-chat-latest with the actual chat-latest
alias; register its context window, output cap, pricing, and cache
rates, and pin explicit rates for legacy gpt-5.x-chat-latest aliases
so the new chat-latest key cannot out-match their cheaper pricing
- Add long-context premium tiers (>272K input) for gpt-5.5 and gpt-5.4
- Disable streaming for pro reasoning models (o1-pro, gpt-5.x-pro),
which OpenAI does not support, with spec coverage
* 🛰️ fix: Address Codex Round-2 Review and CI Spec Failure
- Allow chat-latest through the official OpenAI fetched-model filter
- Export isProReasoningModel and drop unsupported sampling parameters
for versioned pro models (gpt-5.4-pro, gpt-5.5-pro), which the
versioned-model exemption previously let through
- Honor the pro-model streaming disable in both agent chat-completions
routes, which decide SSE from model_parameters before llmConfig exists
- Update models.spec default-list assertions for the refreshed defaults
and cover chat-latest filter retention
* 🛰️ fix: Address Codex Round-3 Review
- Convert max_tokens for chat-latest, which the gpt-[5-9] guard missed
- Drop snake_case sampling params (top_p, logit_bias, penalties) in the
reasoning-model exclusion list so addParams-sourced values are removed
- Add createOpenAIAggregatorHandlers and wire them into the agent
chat-completions service's non-streaming branch, which previously ran
with no handlers and always returned an empty aggregated response
* 🛰️ ci: Fix Import Order Drift and Controller Spec Mock
- Sort type import first in service.spec.ts per import-order convention
- Register isProReasoningModel in the openai controller spec's
@librechat/api mock factory, whose enumerated exports left the new
helper undefined and broke the non-streaming flow under test
* 🛰️ chore: Trim Scope to Model Catalog Changes
Revert the OpenAI endpoint and agent handler changes (pro-model
streaming, sampling exclusions, non-streaming aggregation) — that
surface is moving out of LibreChat into the agents SDK and belongs
in its own change. Keep the model list, token windows, pricing, and
the fetched-model filter for chat-latest.
* 🛰️ fix: Correct GPT-5.4 Context Windows and Pro Long-Context Pricing
- Set gpt-5.4 and gpt-5.4-pro context to the documented 1,050,000
window — 272K is the long-context pricing breakpoint, not the cap,
and using it truncated prompts before they could reach that tier
- Add gpt-5.4-pro long-context premium rates ($60/$270 above 272K)
per its model page; gpt-5.5-pro documents no long-context tier
* 🛰️ fix: Add gpt-5.4-nano and gpt-5.5-pro Long-Context Pricing
- Register gpt-5.4-nano ($0.20/$1.25, cached $0.02, 400K context) in
the model list, pricing, cache, and token maps — the longest-match
fallback billed it at gpt-5.4's $2.50/$15
- Add gpt-5.5-pro long-context premium rates ($60/$270 above 272K);
the pricing table lists the tier even though the model page omits it
* 📦 chore: Bump `jest-junit` to v17.0.0
* 📦 chore: Bump `@librechat/agents` to v3.2.33
* ⚙️ refactor: Lazy-load HEIC upload conversion (#13638)
* ⚙️ refactor: Lazy load locale resources (#13640)
* ⚙️ refactor: brotli asset serving behind a feature toggle (#13641)
* 🪶 fix: Prevent Soft Default Model Spec from Overriding User Selections (#13642)
* 🎯 fix: Soft Default Model Spec Overriding User Selections
* 🎯 fix: Detect Agents-Only Allow-List Before Endpoints Config Loads
* 🎯 fix: Preserve Explicit Soft Default Selections over Older History
* 🎯 fix: Limit Soft Default Residue to Spec-Named State, Disable E2E Enforcement
* 🚰 ci: Close Leaked Redis Clients in Cache Integration Tests (#13649)
* 🧹 fix: Close Leaked Redis Clients in Cache Integration Tests
Importing `redisClients` constructs and connects BOTH `ioredisClient`
and `keyvRedisClient` as module side effects, but most cache/mcp
integration specs disconnected at most one of them — and specs that
re-import the module per test via `jest.resetModules()` leaked a fresh
pair of connected clients (sockets + ping timers) for every test.
On runners where jest resolves to a single worker (2-core machines with
`maxWorkers: '50%'`), the suite runs in-band and the leaked handles keep
the main process alive after all tests pass — the run hangs until the
CI job timeout. On larger runners jest recovers only by force-exiting
the leaked worker ("A worker process has failed to exit gracefully...").
- add a `closeRedisClients()` test helper that settles the connect
promise and closes both clients of a `redisClients` module instance
- call it from every cache/mcp integration spec that creates clients,
mirroring what LeaderElection.cache_integration.spec.ts already does
- remove the rethrow in the `keyvRedisClientReady.catch(...)` logging
handler — rethrowing inside `.catch` creates a new, never-observed
rejected promise, turning any failed initial connect into a
guaranteed unhandled rejection; callers awaiting
`keyvRedisClientReady` still observe the original rejection
All four `test:cache-integration` stages now pass AND exit cleanly with
`--maxWorkers=1` against both single-node and cluster Redis, with no
force-exit warning in worker mode.
* 🧹 chore: Treat testRedisOperations as Assertion in expect-expect Rule
* 🗂️ chore: Sort Imports per Repo Convention
* 🪟 ci: Shard Windows Frontend Unit Tests (#13651)
* 🪟 ci: Shard Windows Frontend Unit Tests
Mirror the 4-way jest sharding the Ubuntu frontend test job already
uses onto the Windows job, which currently runs the whole client suite
in a single 20-minute job. Also drops the `--verbose` flag, which npm
consumed itself (it preceded `--`) and only raised npm's own log level.
* 🪟 ci: Trigger Frontend Tests on Workflow Changes
* 🛡️ feat: Configurable Message PII Filter (#13602)
* 🛡️ feat: Reject chat messages matching configured credential patterns
Adds an opt-in `messagePiiFilter` middleware mounted on the agent
chat route ahead of `moderateText`. When the configured patterns
match the user's input the request is refused with 400, so the
credential never reaches OpenAI moderation, the model, or MongoDB.
Three starter patterns ship by default and operators can subset
them or add their own regex via `customPatterns` in librechat.yaml.
* 🧪 test: Memoize compiled patterns + add middleware spec
Memoize the compiled pattern array via a WeakMap keyed by the
messagePiiFilter config object so repeat requests against the same
config skip the per-request RegExp construction. Cache entries are
released automatically when the config object itself rotates.
Adds packages/api/src/middleware/messagePiiFilter.spec.ts covering
the default-starter rejections, the starterPatterns subset and
empty-array semantics, customPatterns matching layered on top of and
in place of the starters, the no-config and empty-text pass-through
paths, and a memoization regression check.
* 🛡️ fix: Skip invalid customPattern regexes instead of crashing the request
Admin DB overrides for `messagePiiFilter.customPatterns` reach
`req.config` via `mergeConfigOverrides`, which deep-merges raw
override values without re-running `configSchema`. A typo'd regex
like `(` would slip past the YAML-load validation and throw inside
`new RegExp(...)` during `compile()`, returning 500 for every chat
request until the operator rolled the override back.
Wrapped the per-pattern compile in a try/catch that logs the
invalid pattern id + reason and skips it, so other valid patterns
(starters and other custom entries) keep filtering. Added a
regression test alongside the existing spec.
* 🛡️ feat: Extend PII filter to OpenAI-compatible and Responses agent APIs
The chat-route middleware operates on `req.body.text`, but the remote
agent API endpoints (`/api/agents/v1/chat/completions`,
`/api/agents/v1/responses`) accept the same prompt content as a
`messages` array or an `input` field. A caller using their API key
could send a credential-shaped value through either route and bypass
the configured PII filter even though they share the same agent and
model backbone the middleware is meant to guard.
Factored out `findPiiMatchInMessages`, a tolerant walker that handles
both `content: string` and `content: ContentPart[]` user-message
shapes against the same compiled, cached pattern list. Wired it into
the OpenAI-compat controller after agent lookup and into the
Responses controller right after `convertToInternalMessages`. Each
returns the endpoint's native 400 error shape
(`sendErrorResponse` / `sendResponsesErrorResponse`) with the
`message_pii_filter_block` code when a user message matches.
* 🩹 test: Add findPiiMatchInMessages to OpenAI + Responses controller mocks
The OpenAI-compat and Responses controller specs mock `@librechat/api`
with a hand-listed object. The new `findPiiMatchInMessages` export
wired into both controllers in 3ea35af9a was missing from those
mocks, so the production lookup returned undefined and the controllers
threw at request time under jest. Added the missing entries (default
mock: returns null so the handlers fall through to the existing happy
paths). All 278 agents-controller tests pass locally.
* 🧹 refactor: Namespace messagePiiFilter under messageFilter.pii + fix import order
Renames the yaml field `messagePiiFilter` to `messageFilter.pii`, the
module to `messageFilterPii`, the factory to `createMessageFilterPii`,
the type to `MessageFilterPiiConfig`, and the error code to
`message_filter_pii_block`. The wrapper `messageFilter` namespace
gives future safety filters (e.g. `messageFilter.toxicity`) a place
to plug in without restructuring the config later. The
`findPiiMatchInMessages` helper kept its name because it already
describes what it does at the value level.
Also fixes import order Danny flagged on the OpenAI-compatible and
Responses controllers: `findPiiMatchInMessages` was appended at the
bottom of two `require('@librechat/api')` destructures rather than
placed in the length-sorted slot the house style expects.
* 🧹 chore: Length-sort the general require destructure in responses.js
Reorders the general sub-group inside the `require('@librechat/api')`
destructure shortest to longest so the whole block conforms to the
length-sort rule the file's `// Responses API` sub-group already
follows. Pure reorder, no other changes.
* 🧹 chore: Length-sort the defaultConfig block in AppService
Reorders the `defaultConfig` keys in `packages/data-schemas/src/app/service.ts`
shortest-line to longest-line, with the explicit-value entries
(`mcpConfig`, `fileStrategies`, `cloudfront`) trailing the shorthand
ones. Pure reorder, no behavior change.
* ✅ ci: Add mock e2e coverage for agents, prompts, MCP, and chat flows (#13589)
* ✅ Add mock e2e coverage for agents, prompts, MCP, and chat flows
* 🎯 fix: Change enforce modelSpecs to false
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🧭 fix: Mobile Sidebar Navigation on Projects View (#13647)
* 🧭 fix: Mobile Sidebar Navigation on Projects View
* 🧭 fix: Align Sidebar Toggle with JS Mobile Breakpoint at 768px
* 🗜️ ci: Cache Dependencies and Builds in Cache Integration Tests (#13652)
* 🗜️ ci: Cache Dependencies and Builds in Cache Integration Tests
Port the node_modules and package-dist caching pattern from
backend-review.yml to cache-integration-tests.yml, which ran a full
npm ci (~72s) and rebuilt data-provider, data-schemas, and api on
every run. Cache keys are identical to backend-review.yml so the two
workflows share entries. Drops setup-node's npm tarball cache,
superseded by the node_modules restore, matching backend-review.yml.
* 🗜️ ci: Exercise Warm-Cache Path
* 🤗 ci: Cache and Authenticate HF Model Downloads in GitNexus Index (#13653)
* ⚙️ refactor: lazy-load React Query Devtools (#13639)
* perf(client): lazy-load query devtools
* fix: keep query devtools deps lazy
* fix: address query devtools review findings
* fix: exclude query devtools from pwa precache
---------
…
Summary
I fixed skill bundled-file saves so missing metadata upsert results no longer crash the agent workflow or leave uploaded objects behind.
saveSkillFileContentagainst null metadata upsert results and surfacedSKILL_FILE_UPSERT_NOT_FOUNDinstead of dereferencingbytes.upsertSkillFileto use MongoDB modify-result metadata so the saved SkillFile row and insert-vs-replace signal come from one atomic write.Change Type
Testing
cd api && npx jest server/services/Endpoints/agents/skillDeps.spec.js --runInBand --coverage=falsecd packages/data-schemas && npx jest src/methods/skill.spec.ts --runInBand --coverage=false --testNamePattern="SkillFile methods"npx prettier --check api/server/services/Endpoints/agents/skillDeps.js api/server/services/Endpoints/agents/skillDeps.spec.js packages/data-schemas/src/methods/skill.ts packages/data-schemas/src/methods/skill.spec.tsnpx eslint api/server/services/Endpoints/agents/skillDeps.js api/server/services/Endpoints/agents/skillDeps.spec.js packages/data-schemas/src/methods/skill.ts packages/data-schemas/src/methods/skill.spec.tsgit diff --checkTest Configuration:
Checklist