🔁 fix: retry gen_title query on 404 while server is still generating - #13329
Closed
serhiizghama wants to merge 3 commits into
Closed
serhiizghama wants to merge 3 commits into
serhiizghama wants to merge 3 commits into
Conversation
The /gen_title route polls for up to ~15.5 s before returning 404 when the title has not yet been cached. Title generation itself can take up to 45 s, leaving a gap where the client receives a transient 404 and marks the conversation as processed, causing the UI to remain on 'New Chat' for the active session. Replace retry: false with a bounded retry predicate that re-fires the query on HTTP 404 (title still in progress) up to 3 times with a 5 s delay between attempts. Three retries give a combined client window of ~56 s, comfortably covering the 45 s server generation timeout. All other error codes (401, 403, 5xx, network errors) remain non-retried. Closes LibreChat-AI#13318
10 tasks done
danny-avila
added a commit
that referenced
this pull request
Jun 2, 2026
Generate conversation titles as soon as the request is made (in parallel with the response, from the user's first message) as the new default, fixing the #13318 race where a transient /gen_title 404 left new chats stuck on "New Chat". - Add per-endpoint `titleTiming` ('immediate' | 'final') to baseEndpointSchema; `endpoints.all` acts as the global default, unset = immediate. Resolve via a new `resolveTitleTiming` helper (`all` takes precedence). - Fire title generation in parallel with `sendMessage`; `titleConvo` waits (bounded, abortable) for the agent run and titles from the user input only. Persist after the conversation row exists; defer `disposeClient` until the title settles. - Expose `titleGenerationTiming` via startup config; `useTitleGeneration` fetches eagerly in immediate mode with a bounded 404 retry and never treats a transient 404 as final. Skip title queueing for temporary conversations. - Supersedes #13329 while incorporating its bounded 404-retry.
danny-avila
added a commit
that referenced
this pull request
Jun 2, 2026
* ⚡ feat: Immediate Conversation Title Generation Generate conversation titles as soon as the request is made (in parallel with the response, from the user's first message) as the new default, fixing the #13318 race where a transient /gen_title 404 left new chats stuck on "New Chat". - Add per-endpoint `titleTiming` ('immediate' | 'final') to baseEndpointSchema; `endpoints.all` acts as the global default, unset = immediate. Resolve via a new `resolveTitleTiming` helper (`all` takes precedence). - Fire title generation in parallel with `sendMessage`; `titleConvo` waits (bounded, abortable) for the agent run and titles from the user input only. Persist after the conversation row exists; defer `disposeClient` until the title settles. - Expose `titleGenerationTiming` via startup config; `useTitleGeneration` fetches eagerly in immediate mode with a bounded 404 retry and never treats a transient 404 as final. Skip title queueing for temporary conversations. - Supersedes #13329 while incorporating its bounded 404-retry. * 🩹 fix: Address Copilot review findings on title timing - Guard against an undefined conversationId in addTitle (skip + warn) so the gen_title cache key can't collide as `userId-undefined` and saveConvo is never called without a conversationId. - Gate the title `useQueries` on `enabled` so no /gen_title request fires while unauthenticated (e.g. after logout) even if the module queue holds IDs. - Drop the stale `conversationId` param from the titleConvo JSDoc. - Add a regression test for the undefined-conversationId guard. * 🧵 fix: Harden immediate-title edge cases from codex review - Cancel in-flight immediate title generation when the request aborts: thread job.abortController.signal through addTitle so pressing Stop on a new chat neither consumes the title model nor surfaces a title for a cancelled turn. - Preserve a locally-applied title when the final SSE event's conversation carries no title yet (built before the title was saved), so long immediate-mode responses no longer revert the chat to "New Chat" until reload. - Guarantee one full post-completion gen_title fetch cycle before giving up, so a `final`-mode title (generated only after the stream ends) is still fetched under a global `immediate` default instead of being stranded. - Add regression tests for the abort propagation and the undefined-conversationId guard. * 🔁 fix: Correct title abort, post-completion refetch, and replacement ordering Follow-up to codex review of the immediate-title fixes: - Use a dedicated title AbortController instead of `job.abortController`. The latter is also aborted by `completeJob` on *successful* completion, which cancelled any title slower than a short response. The title is now cancelled only on a real user Stop or when the stream is replaced; a completed-then- aborted title is discarded (no save, cache cleared) rather than persisted. - Reset (not remove) the post-completion title query: `resetQueries` refetches the mounted observer with a fresh retry budget, whereas `removeQueries` left it stuck in its error state, so the promised post-completion cycle never ran. - Run the job-replacement check before resolving `convoReady`, and on a replaced stream cancel/discard the stale title so a discarded prompt can't persist a title. * 🧷 fix: Tighten title abort ordering and endpoint-level timing resolution Follow-up to codex review: - Abort the title controller before resolving `convoReady` on a stopped turn, so the title task can't resume and persist before the later abort. - Cancel the title and unblock its waits on ANY send failure (not just user aborts): a preflight/quota failure before the run exists otherwise hangs `_waitForRun`, deferring client disposal until the 45s title timeout. - Resolve `titleTiming` for custom endpoints via `getCustomEndpointConfig` (their config lives under `endpoints.custom[]`, not `endpoints[endpoint]`). - Derive the startup `titleGenerationTiming` via `resolveTitleTiming` for the agents endpoint so an endpoint-level `final` (without `endpoints.all`) is honored client-side instead of defaulting to immediate and burning eager gen_title polls. * 🪢 fix: Per-agent title timing and safer abort/replacement handling Follow-up to codex review: - Resolve `titleTiming` from the agent's actual endpoint after initialization, so a per-endpoint `final` override on a custom/provider endpoint backing an (ephemeral) agent is honored instead of always using the `agents` endpoint's value. - Don't preserve a locally-fetched title on a stopped (unfinished) turn: the server cancels and discards that title, so keeping it client-side would diverge from server state and leave the stopped chat titled until reload. - On abort/replacement, only delete the cached title if it still holds THIS task's value — a replacement stream shares the `userId-conversationId` key and may have already cached its own valid title that must not be removed. * 🪞 fix: Mirror AgentClient title-config resolution for titleTiming Per maintainer guidance, keep titleTiming resolution identical to how `AgentClient#titleConvo` already resolves the endpoint config — `endpoints.all` is the intended global override and the agent's actual provider endpoint is used: - Resolve via `endpoints.all ?? endpoints[endpoint] ?? getProviderConfig(endpoint) .customEndpointConfig` (was using `getCustomEndpointConfig` directly). Going through `getProviderConfig` picks up its case-insensitive fallback for normalized provider names (e.g. `openrouter` → `OpenRouter`), so a custom endpoint's `titleTiming` is honored like its other title settings. - Add `titleTiming` to the Azure endpoint schema `.pick()` so `endpoints.azureOpenAI.titleTiming` is no longer silently stripped by Zod. Note: per-endpoint title settings being skipped when `endpoints.all` is present is the existing, intended global-override behavior — not changed here. * 🧪 test: Cover useTitleGeneration effect logic (integration) Adds a deterministic white-box integration test that drives the real hook's React effects with a controllable react-query surface, locking down the stateful decisions that previously had no coverage: - immediate mode fetches a queued conversation while its stream is still active - final mode gates until the stream completes, then becomes eligible - success applies the fetched title to the conversation caches - a 404 while active defers (removeQueries) instead of giving up - a 404 after completion forces a fresh fetch via resetQueries (post-completion remount) * feat: Stream immediate title events * style: Format title SSE handler * test: Preserve data-provider exports in OAuth mock * test: Isolate OAuth route API mock * test: Keep OAuth callback factory capture * fix: Replay streamed title events on resume * fix: Honor agents title timing precedence * style: Format title timing fixes
Collaborator
|
Closed by #13395 |
fuuuzzy
pushed a commit
to fuuuzzy/LibreChat
that referenced
this pull request
Jun 4, 2026
* ⚡ feat: Immediate Conversation Title Generation Generate conversation titles as soon as the request is made (in parallel with the response, from the user's first message) as the new default, fixing the LibreChat-AI#13318 race where a transient /gen_title 404 left new chats stuck on "New Chat". - Add per-endpoint `titleTiming` ('immediate' | 'final') to baseEndpointSchema; `endpoints.all` acts as the global default, unset = immediate. Resolve via a new `resolveTitleTiming` helper (`all` takes precedence). - Fire title generation in parallel with `sendMessage`; `titleConvo` waits (bounded, abortable) for the agent run and titles from the user input only. Persist after the conversation row exists; defer `disposeClient` until the title settles. - Expose `titleGenerationTiming` via startup config; `useTitleGeneration` fetches eagerly in immediate mode with a bounded 404 retry and never treats a transient 404 as final. Skip title queueing for temporary conversations. - Supersedes LibreChat-AI#13329 while incorporating its bounded 404-retry. * 🩹 fix: Address Copilot review findings on title timing - Guard against an undefined conversationId in addTitle (skip + warn) so the gen_title cache key can't collide as `userId-undefined` and saveConvo is never called without a conversationId. - Gate the title `useQueries` on `enabled` so no /gen_title request fires while unauthenticated (e.g. after logout) even if the module queue holds IDs. - Drop the stale `conversationId` param from the titleConvo JSDoc. - Add a regression test for the undefined-conversationId guard. * 🧵 fix: Harden immediate-title edge cases from codex review - Cancel in-flight immediate title generation when the request aborts: thread job.abortController.signal through addTitle so pressing Stop on a new chat neither consumes the title model nor surfaces a title for a cancelled turn. - Preserve a locally-applied title when the final SSE event's conversation carries no title yet (built before the title was saved), so long immediate-mode responses no longer revert the chat to "New Chat" until reload. - Guarantee one full post-completion gen_title fetch cycle before giving up, so a `final`-mode title (generated only after the stream ends) is still fetched under a global `immediate` default instead of being stranded. - Add regression tests for the abort propagation and the undefined-conversationId guard. * 🔁 fix: Correct title abort, post-completion refetch, and replacement ordering Follow-up to codex review of the immediate-title fixes: - Use a dedicated title AbortController instead of `job.abortController`. The latter is also aborted by `completeJob` on *successful* completion, which cancelled any title slower than a short response. The title is now cancelled only on a real user Stop or when the stream is replaced; a completed-then- aborted title is discarded (no save, cache cleared) rather than persisted. - Reset (not remove) the post-completion title query: `resetQueries` refetches the mounted observer with a fresh retry budget, whereas `removeQueries` left it stuck in its error state, so the promised post-completion cycle never ran. - Run the job-replacement check before resolving `convoReady`, and on a replaced stream cancel/discard the stale title so a discarded prompt can't persist a title. * 🧷 fix: Tighten title abort ordering and endpoint-level timing resolution Follow-up to codex review: - Abort the title controller before resolving `convoReady` on a stopped turn, so the title task can't resume and persist before the later abort. - Cancel the title and unblock its waits on ANY send failure (not just user aborts): a preflight/quota failure before the run exists otherwise hangs `_waitForRun`, deferring client disposal until the 45s title timeout. - Resolve `titleTiming` for custom endpoints via `getCustomEndpointConfig` (their config lives under `endpoints.custom[]`, not `endpoints[endpoint]`). - Derive the startup `titleGenerationTiming` via `resolveTitleTiming` for the agents endpoint so an endpoint-level `final` (without `endpoints.all`) is honored client-side instead of defaulting to immediate and burning eager gen_title polls. * 🪢 fix: Per-agent title timing and safer abort/replacement handling Follow-up to codex review: - Resolve `titleTiming` from the agent's actual endpoint after initialization, so a per-endpoint `final` override on a custom/provider endpoint backing an (ephemeral) agent is honored instead of always using the `agents` endpoint's value. - Don't preserve a locally-fetched title on a stopped (unfinished) turn: the server cancels and discards that title, so keeping it client-side would diverge from server state and leave the stopped chat titled until reload. - On abort/replacement, only delete the cached title if it still holds THIS task's value — a replacement stream shares the `userId-conversationId` key and may have already cached its own valid title that must not be removed. * 🪞 fix: Mirror AgentClient title-config resolution for titleTiming Per maintainer guidance, keep titleTiming resolution identical to how `AgentClient#titleConvo` already resolves the endpoint config — `endpoints.all` is the intended global override and the agent's actual provider endpoint is used: - Resolve via `endpoints.all ?? endpoints[endpoint] ?? getProviderConfig(endpoint) .customEndpointConfig` (was using `getCustomEndpointConfig` directly). Going through `getProviderConfig` picks up its case-insensitive fallback for normalized provider names (e.g. `openrouter` → `OpenRouter`), so a custom endpoint's `titleTiming` is honored like its other title settings. - Add `titleTiming` to the Azure endpoint schema `.pick()` so `endpoints.azureOpenAI.titleTiming` is no longer silently stripped by Zod. Note: per-endpoint title settings being skipped when `endpoints.all` is present is the existing, intended global-override behavior — not changed here. * 🧪 test: Cover useTitleGeneration effect logic (integration) Adds a deterministic white-box integration test that drives the real hook's React effects with a controllable react-query surface, locking down the stateful decisions that previously had no coverage: - immediate mode fetches a queued conversation while its stream is still active - final mode gates until the stream completes, then becomes eligible - success applies the fetched title to the conversation caches - a 404 while active defers (removeQueries) instead of giving up - a 404 after completion forces a fresh fetch via resetQueries (post-completion remount) * feat: Stream immediate title events * style: Format title SSE handler * test: Preserve data-provider exports in OAuth mock * test: Isolate OAuth route API mock * test: Keep OAuth callback factory capture * fix: Replay streamed title events on resume * fix: Honor agents title timing precedence * style: Format title timing fixes
ThomasVuNguyen
pushed a commit
to ThomasVuNguyen/LibreChat
that referenced
this pull request
Jul 15, 2026
* ⚡ feat: Immediate Conversation Title Generation Generate conversation titles as soon as the request is made (in parallel with the response, from the user's first message) as the new default, fixing the LibreChat-AI#13318 race where a transient /gen_title 404 left new chats stuck on "New Chat". - Add per-endpoint `titleTiming` ('immediate' | 'final') to baseEndpointSchema; `endpoints.all` acts as the global default, unset = immediate. Resolve via a new `resolveTitleTiming` helper (`all` takes precedence). - Fire title generation in parallel with `sendMessage`; `titleConvo` waits (bounded, abortable) for the agent run and titles from the user input only. Persist after the conversation row exists; defer `disposeClient` until the title settles. - Expose `titleGenerationTiming` via startup config; `useTitleGeneration` fetches eagerly in immediate mode with a bounded 404 retry and never treats a transient 404 as final. Skip title queueing for temporary conversations. - Supersedes LibreChat-AI#13329 while incorporating its bounded 404-retry. * 🩹 fix: Address Copilot review findings on title timing - Guard against an undefined conversationId in addTitle (skip + warn) so the gen_title cache key can't collide as `userId-undefined` and saveConvo is never called without a conversationId. - Gate the title `useQueries` on `enabled` so no /gen_title request fires while unauthenticated (e.g. after logout) even if the module queue holds IDs. - Drop the stale `conversationId` param from the titleConvo JSDoc. - Add a regression test for the undefined-conversationId guard. * 🧵 fix: Harden immediate-title edge cases from codex review - Cancel in-flight immediate title generation when the request aborts: thread job.abortController.signal through addTitle so pressing Stop on a new chat neither consumes the title model nor surfaces a title for a cancelled turn. - Preserve a locally-applied title when the final SSE event's conversation carries no title yet (built before the title was saved), so long immediate-mode responses no longer revert the chat to "New Chat" until reload. - Guarantee one full post-completion gen_title fetch cycle before giving up, so a `final`-mode title (generated only after the stream ends) is still fetched under a global `immediate` default instead of being stranded. - Add regression tests for the abort propagation and the undefined-conversationId guard. * 🔁 fix: Correct title abort, post-completion refetch, and replacement ordering Follow-up to codex review of the immediate-title fixes: - Use a dedicated title AbortController instead of `job.abortController`. The latter is also aborted by `completeJob` on *successful* completion, which cancelled any title slower than a short response. The title is now cancelled only on a real user Stop or when the stream is replaced; a completed-then- aborted title is discarded (no save, cache cleared) rather than persisted. - Reset (not remove) the post-completion title query: `resetQueries` refetches the mounted observer with a fresh retry budget, whereas `removeQueries` left it stuck in its error state, so the promised post-completion cycle never ran. - Run the job-replacement check before resolving `convoReady`, and on a replaced stream cancel/discard the stale title so a discarded prompt can't persist a title. * 🧷 fix: Tighten title abort ordering and endpoint-level timing resolution Follow-up to codex review: - Abort the title controller before resolving `convoReady` on a stopped turn, so the title task can't resume and persist before the later abort. - Cancel the title and unblock its waits on ANY send failure (not just user aborts): a preflight/quota failure before the run exists otherwise hangs `_waitForRun`, deferring client disposal until the 45s title timeout. - Resolve `titleTiming` for custom endpoints via `getCustomEndpointConfig` (their config lives under `endpoints.custom[]`, not `endpoints[endpoint]`). - Derive the startup `titleGenerationTiming` via `resolveTitleTiming` for the agents endpoint so an endpoint-level `final` (without `endpoints.all`) is honored client-side instead of defaulting to immediate and burning eager gen_title polls. * 🪢 fix: Per-agent title timing and safer abort/replacement handling Follow-up to codex review: - Resolve `titleTiming` from the agent's actual endpoint after initialization, so a per-endpoint `final` override on a custom/provider endpoint backing an (ephemeral) agent is honored instead of always using the `agents` endpoint's value. - Don't preserve a locally-fetched title on a stopped (unfinished) turn: the server cancels and discards that title, so keeping it client-side would diverge from server state and leave the stopped chat titled until reload. - On abort/replacement, only delete the cached title if it still holds THIS task's value — a replacement stream shares the `userId-conversationId` key and may have already cached its own valid title that must not be removed. * 🪞 fix: Mirror AgentClient title-config resolution for titleTiming Per maintainer guidance, keep titleTiming resolution identical to how `AgentClient#titleConvo` already resolves the endpoint config — `endpoints.all` is the intended global override and the agent's actual provider endpoint is used: - Resolve via `endpoints.all ?? endpoints[endpoint] ?? getProviderConfig(endpoint) .customEndpointConfig` (was using `getCustomEndpointConfig` directly). Going through `getProviderConfig` picks up its case-insensitive fallback for normalized provider names (e.g. `openrouter` → `OpenRouter`), so a custom endpoint's `titleTiming` is honored like its other title settings. - Add `titleTiming` to the Azure endpoint schema `.pick()` so `endpoints.azureOpenAI.titleTiming` is no longer silently stripped by Zod. Note: per-endpoint title settings being skipped when `endpoints.all` is present is the existing, intended global-override behavior — not changed here. * 🧪 test: Cover useTitleGeneration effect logic (integration) Adds a deterministic white-box integration test that drives the real hook's React effects with a controllable react-query surface, locking down the stateful decisions that previously had no coverage: - immediate mode fetches a queued conversation while its stream is still active - final mode gates until the stream completes, then becomes eligible - success applies the fetched title to the conversation caches - a 404 while active defers (removeQueries) instead of giving up - a 404 after completion forces a fresh fetch via resetQueries (post-completion remount) * feat: Stream immediate title events * style: Format title SSE handler * test: Preserve data-provider exports in OAuth mock * test: Isolate OAuth route API mock * test: Keep OAuth callback factory capture * fix: Replay streamed title events on resume * fix: Honor agents title timing precedence * style: Format title timing fixes
omarchouikha-goreply
added a commit
to go-reply-de/go-genai-studio
that referenced
this pull request
Jul 30, 2026
* 🛂 fix: Gate RUM Proxy Route on the RUM_ENABLED Flag (#13475)
* ⚡ refactor: Change Minifier from 'terser' to 'oxc' in Vite Config (#13476)
* 🗃️ feat: Retain Agent Files During All-Data Retention (#13477)
* feat: add agent file retention exemption
* refactor: centralize agent file retention policy
* 🩻 refactor: Replace Opaque OAuth Errors with Structured Failure Diagnostics (#13471)
* Improve OAuth failure logging
* Improve OAuth failure logging
* test: type oauth failure request helper
* refactor: move OpenID callback helper to api package
* 🎭 feat: Add Credential-Free Playwright Smoke Suite with a Local Mock LLM (#13472)
* 🧪 feat: add e2e playwright tests
* 🧪 feat: Add Playwright Recording Harness
* test: fix mock playwright config
* test: harden mock e2e environment
* test: preserve mock dotenv secrets
* test: harden mock isolation setup
* ci: cache mock e2e builds
* test: harden e2e cache and recorder checks
* test: preserve data-provider exports in oauth route test
* test: isolate mock auth logout state
* test: allow isolated logout smoke setup
* test: prepare logout smoke auth via api
* test: isolate oauth route module mock
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* ⚡ feat: Immediate Conversation Title Generation (#13395)
* ⚡ feat: Immediate Conversation Title Generation
Generate conversation titles as soon as the request is made (in parallel
with the response, from the user's first message) as the new default,
fixing the #13318 race where a transient /gen_title 404 left new chats
stuck on "New Chat".
- Add per-endpoint `titleTiming` ('immediate' | 'final') to baseEndpointSchema;
`endpoints.all` acts as the global default, unset = immediate. Resolve via
a new `resolveTitleTiming` helper (`all` takes precedence).
- Fire title generation in parallel with `sendMessage`; `titleConvo` waits
(bounded, abortable) for the agent run and titles from the user input only.
Persist after the conversation row exists; defer `disposeClient` until the
title settles.
- Expose `titleGenerationTiming` via startup config; `useTitleGeneration`
fetches eagerly in immediate mode with a bounded 404 retry and never treats
a transient 404 as final. Skip title queueing for temporary conversations.
- Supersedes #13329 while incorporating its bounded 404-retry.
* 🩹 fix: Address Copilot review findings on title timing
- Guard against an undefined conversationId in addTitle (skip + warn) so the
gen_title cache key can't collide as `userId-undefined` and saveConvo is
never called without a conversationId.
- Gate the title `useQueries` on `enabled` so no /gen_title request fires while
unauthenticated (e.g. after logout) even if the module queue holds IDs.
- Drop the stale `conversationId` param from the titleConvo JSDoc.
- Add a regression test for the undefined-conversationId guard.
* 🧵 fix: Harden immediate-title edge cases from codex review
- Cancel in-flight immediate title generation when the request aborts: thread
job.abortController.signal through addTitle so pressing Stop on a new chat
neither consumes the title model nor surfaces a title for a cancelled turn.
- Preserve a locally-applied title when the final SSE event's conversation
carries no title yet (built before the title was saved), so long immediate-mode
responses no longer revert the chat to "New Chat" until reload.
- Guarantee one full post-completion gen_title fetch cycle before giving up, so a
`final`-mode title (generated only after the stream ends) is still fetched under
a global `immediate` default instead of being stranded.
- Add regression tests for the abort propagation and the undefined-conversationId guard.
* 🔁 fix: Correct title abort, post-completion refetch, and replacement ordering
Follow-up to codex review of the immediate-title fixes:
- Use a dedicated title AbortController instead of `job.abortController`. The
latter is also aborted by `completeJob` on *successful* completion, which
cancelled any title slower than a short response. The title is now cancelled
only on a real user Stop or when the stream is replaced; a completed-then-
aborted title is discarded (no save, cache cleared) rather than persisted.
- Reset (not remove) the post-completion title query: `resetQueries` refetches
the mounted observer with a fresh retry budget, whereas `removeQueries` left it
stuck in its error state, so the promised post-completion cycle never ran.
- Run the job-replacement check before resolving `convoReady`, and on a replaced
stream cancel/discard the stale title so a discarded prompt can't persist a title.
* 🧷 fix: Tighten title abort ordering and endpoint-level timing resolution
Follow-up to codex review:
- Abort the title controller before resolving `convoReady` on a stopped turn, so
the title task can't resume and persist before the later abort.
- Cancel the title and unblock its waits on ANY send failure (not just user
aborts): a preflight/quota failure before the run exists otherwise hangs
`_waitForRun`, deferring client disposal until the 45s title timeout.
- Resolve `titleTiming` for custom endpoints via `getCustomEndpointConfig`
(their config lives under `endpoints.custom[]`, not `endpoints[endpoint]`).
- Derive the startup `titleGenerationTiming` via `resolveTitleTiming` for the
agents endpoint so an endpoint-level `final` (without `endpoints.all`) is honored
client-side instead of defaulting to immediate and burning eager gen_title polls.
* 🪢 fix: Per-agent title timing and safer abort/replacement handling
Follow-up to codex review:
- Resolve `titleTiming` from the agent's actual endpoint after initialization, so a
per-endpoint `final` override on a custom/provider endpoint backing an (ephemeral)
agent is honored instead of always using the `agents` endpoint's value.
- Don't preserve a locally-fetched title on a stopped (unfinished) turn: the server
cancels and discards that title, so keeping it client-side would diverge from
server state and leave the stopped chat titled until reload.
- On abort/replacement, only delete the cached title if it still holds THIS task's
value — a replacement stream shares the `userId-conversationId` key and may have
already cached its own valid title that must not be removed.
* 🪞 fix: Mirror AgentClient title-config resolution for titleTiming
Per maintainer guidance, keep titleTiming resolution identical to how
`AgentClient#titleConvo` already resolves the endpoint config — `endpoints.all`
is the intended global override and the agent's actual provider endpoint is used:
- Resolve via `endpoints.all ?? endpoints[endpoint] ?? getProviderConfig(endpoint)
.customEndpointConfig` (was using `getCustomEndpointConfig` directly). Going
through `getProviderConfig` picks up its case-insensitive fallback for normalized
provider names (e.g. `openrouter` → `OpenRouter`), so a custom endpoint's
`titleTiming` is honored like its other title settings.
- Add `titleTiming` to the Azure endpoint schema `.pick()` so
`endpoints.azureOpenAI.titleTiming` is no longer silently stripped by Zod.
Note: per-endpoint title settings being skipped when `endpoints.all` is present is
the existing, intended global-override behavior — not changed here.
* 🧪 test: Cover useTitleGeneration effect logic (integration)
Adds a deterministic white-box integration test that drives the real hook's
React effects with a controllable react-query surface, locking down the
stateful decisions that previously had no coverage:
- immediate mode fetches a queued conversation while its stream is still active
- final mode gates until the stream completes, then becomes eligible
- success applies the fetched title to the conversation caches
- a 404 while active defers (removeQueries) instead of giving up
- a 404 after completion forces a fresh fetch via resetQueries (post-completion remount)
* feat: Stream immediate title events
* style: Format title SSE handler
* test: Preserve data-provider exports in OAuth mock
* test: Isolate OAuth route API mock
* test: Keep OAuth callback factory capture
* fix: Replay streamed title events on resume
* fix: Honor agents title timing precedence
* style: Format title timing fixes
* 🧭 ci: Use System Chrome for Mock E2E (#13481)
* 🌍 i18n: Update translation.json with latest translations (#13482)
* 🏷️ fix: Prevent Bedrock Cache Tokens from Inflating Completion Count (#13468)
* 🐛 fix: prevent Bedrock cache tokens from inflating completion count
* style: fix prettier formatting
* 🛰️ feat: Add Auth Fallback Observability (#13488)
* feat: add auth fallback observability
* refactor: move auth log helpers to api package
* test: harden auth log context handling
* fix: keep auth logs low cardinality
* fix: render auth log context in debug messages
* fix: lower plain jwt auth failures to debug
* 🔗 feat: Add Granular Access Control to Shared Links via ACL System (#13051)
* feat: Add granular access control to shared links via ACL system
* fix(shared-links): preserve isPublic on failed migration grants
Transient ACL failures during auto-migration permanently stranded
links — $unset ran unconditionally, removing the legacy flag that
triggers retry. Now only $unset isPublic after all grants succeed.
* fix(config): skip isPublic unset for failed ACL grants
Bulk migration unconditionally removed isPublic from all links,
even those whose ACL writes failed. Failed links then lost the
legacy marker needed for auto-migration retry. Now tracks failed
link IDs per-batch and excludes them from the $unset step.
Also adds sharedLink to AccessRole resourceType schema enum —
was missing, only worked because seedDefaultRoles uses
findOneAndUpdate which bypasses validation.
* ci(config): add jest config and PR workflow for migration tests
config/__tests__/ specs depend on api/jest.config.js module
mappings but had no dedicated runner. Adds config/jest.config.js
extending api config with absolutized paths, npm test:config
script, and a GitHub Actions workflow triggered by changes to
config/, api/models/, api/db/, or packages/ ACL code.
* fix(permissions): honor boolean sharedLinks config
SHARED_LINKS has no USE permission, so boolean config produced
an empty update payload — gate conditions only matched object
form, making `sharedLinks: false` a no-op on existing perms.
* fix(share): resolve role before creating shared link
Role lookup between create and grant left an orphaned link
without ACL entries if getRoleByName threw — retry then hit "Share already exists" with no recovery path.
* fix: Restore Public ACL Access Checks
* fix: Type Public ACL Lookup
* fix: Preserve Private Legacy Shared Links
* chore: Promote Shared Link Permission Migration
* fix: Address Shared Link Review Findings
* fix: Repair Shared Link CI Follow-Up
* fix: Narrow Shared Link Mongoose Test Mock
* fix: Address Shared Link Review Follow-Ups
* fix: Close Shared Link Review Gaps
* fix: Guard Missing Shared Link Permission Backfill
* test: Add Shared Link Mock E2E
* test: Stabilize Shared Link Mock E2E
---------
Co-authored-by: Danny Avila <danny@librechat.ai>
* 🗂️ feat: Add Private Chat Projects (#13467)
* feat: Add private chat projects
* fix: Format project files
* fix: Address project review findings
* fix: Resolve project review follow-ups
* fix: Handle project stats and cache edge cases
* style: align projects UI with sidebar patterns
* fix: resolve projects UI lint issues
* style: Align project menus and composer
* fix: Avoid project placeholder shadowing
* fix: Handle project search and stale ids
* fix: Polish project sidebar behavior
* fix: Preserve new chat stream after creation
* fix: Stabilize project sidebar sections
* fix: Smooth project sidebar organization
* fix: stabilize project chat entry
* fix: keep project workspace outside chat context
* fix: show default model on project workspace
* fix: fallback project workspace model label
* fix: preserve project scope during draft hydration
* fix: include route project in new chat submission
* fix: persist project id in agent chat saves
* fix: refine project sidebar and creation UX
* fix: export chat project method types
* fix: polish project landing context
* fix: refine project navigation affordances
* feat: rework projects UX — coexisting sidebar sections + URL-driven scope
Sidebar
- Replace the chronological/by-project mode toggle with coexisting
Projects + Chats sections (both always visible)
- Remove ProjectConversations (927 lines), the org-mode Header, and types
- Add ProjectsSection: collapsible project rows that unfurl chats inline
(full-size rows), with per-project new chat and an open/rename/delete menu
- Lift the marketplace/favorites shortcuts above the Projects section
Chat scope
- Derive a new chat's project strictly from the URL ?projectId, so the
global New Chat no longer stays stuck in a project after a project chat
Surfaces
- Chat landing: subtle, clickable project chip instead of the floating badge
- Project workspace: modest header, composer-style entry, chats list
- All-projects grid: Claude-style cards with pluralized chat counts
* chore: prune unused i18n keys; fix project chat-count pluralization
* fix: project new-chat keeps model spec; sidebar header + row polish
- newConversation: ignore a chatProjectId-only template when deciding to
apply the default model spec, so starting a chat in a project no longer
strips the conversation `spec`
- useSelectMention: the Model Selector and @ command now retain the active
project across endpoint/spec/preset switches; other new-chat paths still
clear it
- Chats header now matches the Projects header (inline chevron + a new-chat
icon button) and starts a non-project chat
- Project rows: use the new-chat icon for the per-project add button, render
at text-sm to match the chat list, and align the row actions + hover color
with conversation rows
* fix: read project scope from router params; align sidebar header icons
- useSelectMention now reads the active project from React Router's search
params instead of window.location, which can drift out of sync because
new-chat params are written to the URL via raw history.pushState; the
Model Selector and @ command now reliably keep the project on switch
- Move the Chats section header out of the virtualized list so it renders
in the same context as the Projects header and isn't shifted by the
list scrollbar
- Inset header action icons (pr-2) so Projects/Chats header icons line up
with the project-row and conversation-row trailing actions
- Extract getRouteChatProjectId into utils for the submit path
* fix: preserve chatProjectId through the new-chat template reduction
The param-endpoint guard in newConversation reduced a new chat's template to
{ endpoint } only, dropping the chatProjectId injected by the Model Selector /
@ switch — so switching models cleared the project scope. Keep chatProjectId
in the reduced template.
* style: align chat-history panel top padding; improve projects page contrast
- Add pt-2 to the chat-history panel so its top spacing matches the other
side panels (agent builder, skills, files, etc.)
- Projects grid + workspace now use the darkest surface for the page
(surface-primary) with cards, inputs, and the composer one step lighter
(surface-secondary) and tertiary on hover, so cards read as elevated
rather than darker than the background
* feat: interactive project landing chip + gallery icon for all-projects
- All-projects sidebar button uses the gallery-vertical-end icon
- The project landing chip is now interactive: click it to switch projects
via a searchable combobox (ControlCombobox), or the trailing × to drop the
project scope. Both update the draft conversation and the ?projectId search
param in place, so the typed message and selected model are preserved
* test: fix Conversations unit test for refactored sidebar; add projects e2e
- Update Conversations.test.tsx mocks for the inline Chats header
(useNewConvo, useQueryClient, conversation atom, NewChatIcon, TooltipAnchor),
drop the removed chatsHeaderControls prop, and remove the mock for the
deleted ../Header module — fixes the failing frontend Jest job
- Add e2e/specs/mock/projects.spec.ts covering project creation, the
project-scoped new-chat landing + interactive chip (switch/remove), and
listing projects on /projects
- Give the landing chip combobox a stable selectId for reliable targeting
* fix: refresh project stats after project-chat activity; stabilize e2e
- useEventHandlers: when a project chat is created/updated, invalidate the
live [projects] query (gated on chatProjectId) instead of the now-unused
projectConversations key, so the sidebar + all-projects stats refresh
after a streamed reply (addresses a Codex finding)
- projects e2e: assert the reliable project-landing behavior (chip, scoped
composer, accepted send) rather than the /c/:id transition, which the
mock LLM harness doesn't complete
* test: verify a project chat saves and is filed under its project (e2e)
- Switch to a mock endpoint before sending so the message streams without a
real API key (the default model failed with "No key found", so no chat was
saved and the page never left /c/new); this also asserts the project chip
survives the model switch
- Restore the reply + /c/:id transition assertions and add a check that the
chat is listed under the expanded project in the sidebar
- Add data-testid="project-chats-<id>" to the inline project chat list
* fix: address Codex review findings (project scope edge cases)
- useSelectMention: fall back to the conversation's chatProjectId when the
URL has no projectId, so switching model/spec inside an existing project
chat (/c/:id) keeps the project assignment
- Conversations: include chatProjectId in the MemoizedConvo comparator so a
sidebar row's project menu doesn't stay stale after a reassignment
- useDeleteProjectMutation: clear the active conversation's chatProjectId
when its project is deleted (mirrors the assignment mutation); drop the
now-dead projectConversations invalidation
- useQueryParams: carry the project into the new conversation when applying
URL settings, so /c/new?projectId=...&<settings> stays scoped
* fix: project stats pagination + archived-chat edge cases (data-schemas)
- listChatProjects: include the null lastConversationAt bucket in the desc
cursor so empty projects paginate (a $lt:<date> predicate excluded nulls,
hiding chat-less projects from "Load more")
- saveConvo: recompute project stats instead of the incremental fast path
when the saved conversation is itself archived/temporary/expired, so a
project's lastConversationAt/Id no longer points at a hidden chat
* test: cover chat-less project pagination across the dated→null boundary
* fix: validate project ownership in bulkSaveConvos
Bulk paths (import/duplicate/fork) persisted whatever chatProjectId the
payload carried; an id that does not belong to the user created an orphan
assignment hidden from both the project and the unassigned sidebar. Validate
ownership like saveConvo and strip un-owned project ids before persisting,
refreshing stats only for owned projects.
* fix(projects): preserve chatProjectId on continuation, basename-safe delete redirect, project-detail invalidation
* fix(projects): navigate project workspace chats via useNavigateToConvo to avoid stale conversation state
* fix(projects): include projectConversations cache when resolving deleted chat's project for detail invalidation
* fix(projects): refresh both projects when a save or bulk write moves a chat between them
* style(projects): use Folders icon for the sidebar Projects header
* fix(projects): require id on ProjectUser so ProjectRequest extends Express Request cleanly
* style(projects): taller project chip with hover-revealed remove button, upward combobox; sort en translations
* style(projects): show endpoint/agent icon for project workspace chat rows
* 🎚️ feat: Add Focus Management and Drag-to-Scrub to MessageNav (#13497)
* 🧭 feat: Add MessageNav Focus Management and Drag-to-Scroll
Resolves #13491: move keyboard focus into the conversation when a message indicator is selected, and add a Shift+Alt+M shortcut to jump focus back to the nav. Also adds drag-to-scrub interaction across the rib column.
* 🖱️ style: Use grab cursor for MessageNav drag affordance
* 🐛 fix: Harden MessageNav drag against stale pointer state and dead clicks
Addresses Codex/Copilot review on #13497:
- Ignore pre-drag pointermoves when the primary button is not held, preventing a stale press (released outside the column) from starting a spurious scrub or calling setPointerCapture on an inactive pointer.
- Clear the post-drag click-suppression flag after the synthetic-click window so a later activation (including keyboard) is never swallowed.
- Match the advertised Shift+Alt+M shortcut by accepting the layout-aware key in addition to the physical code.
* 🎯 fix: Track MessageNav drag globally so it survives leaving the column
Round-2 Codex review on #13497: the 4px threshold was applied before any pointer capture, so a drag that left the narrow rib column before crossing it silently failed to scrub.
Replace per-element capture with document-level pointermove/up/cancel listeners attached on pointerdown:
- Drag tracking continues regardless of pointer position (fixes diagonal/touch drags off the ribs).
- pointerup is always received, so no stale drag state and no setPointerCapture on an inactive pointer (removes the NotFoundError path entirely).
- Native click is preserved, so the keyboard/click selection a11y path is unchanged.
- Listeners are torn down on pointerup/cancel and on unmount.
* 🧹 fix: Reset drag state on pointer replace and gate MessageNav shortcut
Round-3 Codex review on #13497:
- When a second pointerdown replaces an in-progress drag, run the cleanup with the real drag state so draggingRef is cleared and the rib column resumes auto-centering (was hardcoded to finish(false)).
- Only preventDefault on Shift+Alt+M when the nav is actually rendered and has a focus target, so the shortcut no longer swallows browser/AT shortcuts when the nav is absent (<3 messages). focusNav now reports whether it moved focus.
* ✨ fix: Make MessageNav drag span the whole thread and harden teardown
Round-4 Codex review on #13497:
- Map the drag pointer proportionally across the full entries range instead of the visible rib rects, so long conversations whose mini-nav overflows are fully scrubbable in one drag. This is also wobble-immune, so the column auto-centering no longer needs to be frozen mid-drag (removed the freeze and draggingRef).
- focusNav now reports success only if focus actually landed, so Shift+Alt+M does not preventDefault when the nav is mounted-but-hidden (hidden md:flex on small viewports).
- End the drag if the primary button is released mid-move or the window loses focus, covering pointers released outside the document where pointerup/cancel never arrive.
* 🪤 fix: Reload Messages When Reopening a Chat from a Non-Chat Route (#13501)
navigateToConvo now removes the target conversation's cached messages before
fetching, so a freshly-mounted ChatView refetches them. clearMessagesCache
leaves a left conversation cached as [], and the messages query's
refetchOnMount: false treats that empty array as valid — so returning to the
conversation from a route where ChatRoute was unmounted (e.g. /projects) left
the chat stuck on an empty cache with no /api/messages request.
* 🗂️ feat: Add Agent File Authoring Tools (#13435)
* feat: add agent file authoring tools
* style: format file authoring changes
* style: satisfy file authoring prettier
* test: fix file authoring initialization expectations
* fix: complete skill file authoring flow
* fix: pass skill authoring state on edit
* test: mock missing bundled skill file
* fix: harden agent file authoring gates
* fix: preserve file authoring runtime context
* test: fix authoring context mock typing
* fix: preserve subagent skill primes
* test: avoid array at in handler spec
* refactor: deepen skill authoring runtime wiring
* fix: address codex authoring review findings
* test: fix authoring collision fixture type
* test: add skill file authoring mock e2e
* fix: Improve skill file authoring recovery
* fix: Show file authoring args while running
* fix: Clarify skill rename authoring errors
* fix: Keep code-only file authoring schemas sandbox scoped
* fix: Address skill authoring review findings
* fix: Gate skill authoring on write access
* 🚦 fix: Gate Chat Starts During Readiness (#13502)
* fix: guard chat starts during server readiness
* style: format readiness retry condition
* fix: clarify chat start retry diagnostics
* fix: cancel stale chat start retries
* style: use const for retry timeout
* 🌍 i18n: Update translation.json with latest translations (#13505)
* 🎭 test: Run Mock E2E Suite Through `createRun` With In-Process Fake Model (#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 (#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 (#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 (#13512)
LibreChat recently updated Vite (see 7dba640c9).
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 (#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 #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 #13512 is merged.
* 🪧 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
* 📜 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: 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 …
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A race between the server-side title generation window (45 s) and the
/gen_titlepolling window (~15.5 s) causes conversations to staylabelled New Chat for the active session.
When the first
GET /api/convos/gen_title/:conversationIdcall exhaustsits wait and returns
404, the client-side query hasretry: false, soit immediately marks the conversation as processed and removes it from
the title queue. If the title finishes generating on the server after
that point, the UI never picks it up — the user must reload the page.
Relevant source locations (identified in #13318):
api/server/routes/convos.js—/gen_titleroute polls for[500, 1000, 2000, 4000, 8000]ms (~15.5 s total) then returns 404api/server/services/Endpoints/agents/title.js— generation timeoutis 45 000 ms
client/src/data-provider/SSE/queries.ts— title query configuredwith
retry: falseSolution
Replace
retry: falsewith a predicate that retries only on HTTP 404(title still in progress on the server) capped at 3 attempts with a
5 s delay between each.
Combined window: ~56 s, safely covering the 45 s server generation
timeout. All other error codes (401, 403, 5xx, network errors) remain
non-retried —
isErroris set only after retries are exhausted, so theexisting "mark as processed" guard in the error handler still works
correctly.
Files changed
client/src/data-provider/SSE/queries.ts— swapretry: false→bounded 404-only retry predicate +
retryDelay: () => 5_000client/src/data-provider/SSE/__tests__/queries.test.ts(new) —16 tests covering
genTitleQueryKey,queueTitleGeneration, errorclassification and the failure cap
Testing
Closes #13318