Commit graph

762 commits

Author SHA1 Message Date
Danny Avila
1662adc581
📺 feat: Google URL Context Param with Native YouTube Video Understanding (#13924)
*  feat: Add Google url_context Param with Native YouTube Video Understanding

Mirror the web_search grounding wiring for a new Google/Gemini `url_context`
model param (resolves to the native `urlContext` tool). When enabled, YouTube
URLs in the latest user message are injected as Gemini video parts (fileData),
since the URL Context tool does not support YouTube.

* 🎞️ fix: Provider-aware YouTube injection limits for url_context

Address Codex review on the YouTube video-understanding path:
- Cap injected YouTube parts per request by provider/model (Vertex: 1; Gemini
  Developer API: 10 on 2.5+, 1 on earlier models) so multi-link messages cannot
  exceed the provider limit and get rejected.
- Set a video/mp4 mimeType on Vertex YouTube fileData (matching Vertex samples);
  the Developer API still omits it.

* 🧩 fix: Round-trip url_context for Google-compatible custom endpoints

Add url_context to openAIBaseSchema so the per-chat value persists for custom
endpoints configured with customParams.defaultParamsEndpoint: 'google', matching
how web_search is already picked there.

* 🚦 fix: Gate url_context tool to Gemini 2.5+ models

Per Google's URL Context supported-models list (2.5+/3.x only), skip the native
urlContext tool on earlier models (debug-log + no-op) instead of sending it and
triggering a provider 400. This also gates the coupled YouTube video-understanding
injection to 2.5+, since it keys off the resolved urlContext tool.

* ✂️ fix: Strip YouTube URLs from urlContext text; keep url_context out of OpenAI schema

- Remove url_context from the shared openAIBaseSchema (revert): it is Google-only
  and would otherwise leak as an unsupported param to OpenAI/Azure/OpenRouter
  requests. On Google-compatible custom endpoints url_context is enabled via admin
  addParams/defaultParams, same as web_search.
- When injecting YouTube video parts, strip the matched YouTube URLs from the prompt
  text so the urlContext tool (which reads URLs from text and cannot fetch YouTube)
  does not consume its URL budget on them. Non-YouTube URLs are left intact.

* 🎯 fix: Refine url_context model gating and YouTube injection edges

Address Codex round 4:
- Exclude non-text modality variants (image/live/tts) from URL Context support,
  mirroring the Google tool-combination modality exclusion.
- Use the resolved run model (model_parameters.model) for YouTube injection limits
  instead of the saved base model.
- Strip only the YouTube links actually routed to video (id-aware); keep over-limit
  links in the text so the model can still reason about them.
- Keep timestamped YouTube links (?t=/&start=) in the text so the moment cue survives.
- Recognize youtube-nocookie.com/embed links.

* 🎚️ fix: Exclude audio Gemini variants + preserve pre-id YouTube timestamps

Address Codex round 5:
- Add `audio` to the url_context modality exclusion so audio-only Gemini variants
  (e.g. gemini-2.5-flash-preview-native-audio-dialog) skip the tool instead of 400ing.
- Detect YouTube timestamps anywhere in the matched URL (incl. before `v=`, e.g.
  watch?t=90&v=<id>), so timestamped links are kept in the prompt text as intended.
2026-06-23 22:42:06 -04:00
Danny Avila
d9a76fca90
🧠 feat: Configurable Reasoning Replay for Custom Endpoints (#13921)
* 🧠 feat: Configurable Reasoning Replay for Custom Endpoints

Adds customParams.includeReasoningContent so OpenAI-compatible custom endpoints (e.g. Xiaomi MiMo, Kimi) can replay reasoning_content on tool-call turns natively, without impersonating the moonshot provider.

* 🔁 feat: Replay reasoning_content across turns for opted-in custom endpoints

Extends the DeepSeek reasoning-content format spoof to honor customParams.includeReasoningContent, so custom OpenAI-compatible endpoints (Xiaomi MiMo, Kimi) reconstruct reasoning_content from persisted history on later turns, matching DeepSeek thinking-mode parity. Adds shouldReplayReasoningContent predicate (tested) and surfaces the flag on the initialized agent.

* 🪢 refactor: Split within-run vs cross-turn reasoning replay flags

moonshot only replays reasoning_content within a run's tool calls, not across turns. Decouples the two: includeReasoningContent = within-run replay (exact moonshot parity), new includeReasoningHistory = cross-turn reconstruction from persisted history (implies includeReasoningContent, since reconstruction is a no-op without the within-run replay flag).

* 🩹 fix: Apply reasoning replay across all param-format branches

Move the within-run includeReasoningContent application out of the OpenAI-only branch in getOpenAIConfig to after the branch dispatch, so custom endpoints using anthropic/google defaultParamsEndpoint gateway modes also honor includeReasoningContent/includeReasoningHistory. Addresses Codex finding.

* chore: Update @librechat/agents to v3.2.46

* 🧽 refactor: De-spoof reasoning replay via explicit preserveReasoningContent

Now that @librechat/agents 3.2.46 exposes an explicit preserveReasoningContent option on formatAgentMessages, pass it directly instead of impersonating provider: deepseek. Behavior is unchanged (shouldReplayReasoningContent still gates DeepSeek + the custom includeReasoningHistory flag); also corrects the comment to reference includeReasoningHistory.

* 🌳 fix: Walk subagents in the reasoning-history replay gate

The gate only checked the primary agent and top-level handoff/parallel configs, so an opted-in custom endpoint used solely as a nested subagent had its persisted reasoning dropped on later turns. New exported anyAgentReplaysReasoningContent walks subagentAgentConfigs (cycle-safe, mirrors anyAgentHasCodeEnv); client.js uses it. Addresses Codex finding.
2026-06-23 21:08:47 -04:00
Danny Avila
e807c63d5d
🔐 fix: Gate Shared Startup Config By Link Access (#13897)
* fix: gate shared startup config by link access

* fix: satisfy shared config CI checks

* fix: align shared config client types

* fix: reject expired shared link access
2026-06-23 08:28:37 -04:00
Danny Avila
3945533a4e
🏷️ chore: Bump Individual Package Versions (#13891) 2026-06-22 09:54:19 -04:00
Danny Avila
5eb1c2c107
🖇️ feat: Reference Selected Chat Text with Multi-Quote Popup (#13868)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🖇️ feat: Reference Selected Chat Text with Multi-Quote Popup

Add a ChatGPT/Codex-style quote feature: selecting text in any message shows
an 'Add to chat' popup that accumulates removable quote chips above the
composer. On submit, the excerpts are merged into the user message text as
Markdown blockquotes (counted in the user message token count, not a system
message) and persisted on the message so they render on the user bubble and
survive reload.

- packages/api: add getReferencedQuotes + mergeQuotedText helpers (blockquote merge, length/count caps) with unit tests
- BaseClient.sendMessage: temporarily merge req.body.quotes into userMessage.text before buildMessages, restore clean text, persist quotes array
- data-schemas + data-provider: add optional quotes field to message schema/type
- client: pendingQuotesByConvoId atom, QuoteButton selection popup, PendingQuoteChips composer row, MessageQuotes persistent display
- useChatFunctions: drain pending quotes onto the message, carry forward on regenerate
- add localization keys and component/integration tests

* 🧪 test: Add Playwright e2e for chat quote feature

Add e2e/specs/mock/quotes.spec.ts covering select -> 'Add to chat' popup ->
chip -> send -> persistent reference block -> reload, plus multi-select
accumulation and chip removal. Selection is driven programmatically (real DOM
Range + dispatched mouseup) to summon the popup deterministically.

Add data-testid hooks (add-to-chat-button, pending-quote-chips, message-quotes)
to the quote components for stable selectors.

* 🛡️ fix: Address Codex review on quote feature

- Run PII filter + OpenAI moderation over req.body.quotes (P1): quoted excerpts
  are merged into the model-facing user message, so they must clear the same
  filters; a crafted quotes payload could otherwise bypass them. Adds tests.
- Carry quotes through edit/save-and-submit replays (overrideQuotes in
  EditMessage), mirroring overrideManualSkills, so edited turns keep context.
- Hide the quote UI for Assistants endpoints (which bypass BaseClient merge),
  so users can't queue quotes the assistant never receives.
- Clear pending quote/skill queues by resolved conversationId in useClearStates,
  not the UI index, so queued-but-unsent selections don't linger in Recoil.
- Cap queued quotes client-side at 10 to match the backend QUOTE_MAX_COUNT, so
  the composer never shows more quotes than are actually sent.

* 🧵 fix: Durably re-merge quotes + Codex round 2

Address Codex's re-review of the quote feature:

- Durable history re-merge (per maintainer decision): quotes are no longer
  merged at request time and stripped; instead each user message's persisted
  message.quotes is merged into its formatted content in AgentClient.buildMessages
  (new prependQuotes helper) for current AND historical turns. The model
  receives the referenced context on every prompt and the token count stays
  consistent with what was persisted; stored text stays clean for display.
- Attach normalized quotes to the user message in handleStartMethods (before
  getReqData/onStart) so the optimistic bubble, resumable abort metadata, and
  saved row all carry them (fixes the abort-metadata gap).
- Skip the quote drain entirely for Assistants endpoints in useChatFunctions,
  leaving the pending atom intact (UI is already hidden there).
- Normalize req.body.quotes via getReferencedQuotes before moderation/PII so
  only the trimmed/truncated/capped excerpts the model will receive are checked.
- Tests: prependQuotes unit tests; BaseClient quote tests assert early
  attachment + clean text; e2e now verifies the model receives the merged
  blockquote on the current turn and re-merged from history on a later turn
  (new E2E_ASSERT_QUOTE mock marker).

* 🔗 fix: Quote share/memo/abort/PII gaps (Codex round 3)

- Shared links: include quotes in the anonymized projection + SharedMessage
  type (+test) so the /share view renders the same reference blocks as the
  owner, mirroring manualSkills/alwaysAppliedSkills.
- MessageRender memo: compare quotes length so a server/resume copy whose only
  change is the quote list re-renders (the block no longer goes stale/missing).
- Resumable job metadata: include quotes in the userMessage written to
  GenerationJobManager so a reload/reconnect mid-stream reconstructs the chips.
- PII + moderation: also scan the merged blockquote+text exactly as the model
  receives it, so a secret split across a quote and the typed body (each clean
  alone) is caught (+cross-boundary test).
- e2e: make quote-add robust against the auto-scroll-dismisses-selection race
  via a retried select+click helper.

* 🛑 fix: Keep quotes on aborted turn's request message (Codex round 4)

abortMiddleware reconstructs finalEvent.requestMessage from jobData.userMessage
but only copied ids + text; include quotes so a stopped quoted turn keeps its
MessageQuotes in the UI and a regenerate-before-reload still sends the
referenced context. Completes the resumable-metadata fix from the prior round.

* 🧮 fix: Quote recount + preliminary abort metadata (Codex round 5)

- Force a canonical token recount for messages carrying quotes in
  AgentClient.buildMessages, so a plain text-only Save edit (which recomputes
  tokenCount from text alone) can't leave a stale, quote-excluding count that
  undercounts context on later turns — recount from the quote-merged copy
  self-heals it.
- Seed normalized quotes into the preliminary userMessage metadata
  (getPreliminaryUserMessage), so an abort during init/tool-loading (before
  onStart) still reconstructs the stopped turn's MessageQuotes.

*  fix: Add getReferencedQuotes to controller test mocks (CI)

request.js's getPreliminaryUserMessage now calls getReferencedQuotes; the
agents controller specs mock @librechat/api wholesale, so the mock must export
it or the call throws and cascades. Added a faithful mock (normalize/cap,
null when empty) to request.resumeMetadata.spec.js and jobReplacement.spec.js.

* 📐 fix: Quotes in context projection + resumable metadata (Codex round 6)

- Context-usage projection (resolveContextProjection): select message.quotes,
  prepend them into the projected user text, and recount quoted messages so the
  context gauge counts the same prompt the model receives (a text-only Save edit
  no longer makes the gauge undercount / over-report remaining budget).
- Resumable job metadata: trackUserMessage (created-event rewrite) and abortJob
  (final requestMessage) now carry quotes; SerializableJobData.userMessage and
  CreatedEvent.message gained an optional quotes field. With the cross-replica
  created-event spread, stopping/reconnecting a quoted turn after the created
  event keeps its MessageQuotes.

* 💬 feat: Collapse multi-select quotes into one chip with hover popup

Composer feedback: the quote chip area now shows a single chip — the excerpt
text for one selection, or a collapsed "{n} selections" pill for multiple,
with a hover popup (HoverCard) listing every excerpt and a per-item remove. The
chip is taller (py-1.5/text-sm) to read less skinny. Adds com_ui_quote_selections
and com_ui_remove_all_quotes; updates unit + e2e tests (e2e drives the count via
a data-quote-count hook and exercises the hover popup).

*  fix: Make multi-selection quote popup keyboard accessible

The collapsed "{n} selections" pill used a HoverCard, which Radix only opens on
pointer hover — its interactive content was unreachable by keyboard. Replaced it
with a Popover: the trigger is a real button that opens on click / Enter / Space
(focus moves into the list, each excerpt's × is tab-navigable, Escape closes and
restores focus), with hover-open preserved for mouse via controlled open state +
a close grace period. Hover-initiated opens skip auto-focus so they don't pull
focus off the composer. Adds an e2e asserting keyboard open/close.

* 📐 fix: Clamp the Add-to-chat button within the viewport (Codex round 7)

The floating selection button positioned via translate(-50%,-100%) (bottom-center
anchor) but clamped top/left as if they were its top-left, so a selection near
the viewport top or sides could render the button partly/fully offscreen. Now it
measures the button (ref + useLayoutEffect) and computes an on-screen top-left —
clamping by the full width within side margins and flipping below the selection
when there's no room above — with no transform, and stays hidden until measured
so it never flashes at an unclamped spot.

* ↩️ fix: Restore pending quotes on early-abort draft (Codex round 8)

When a turn is stopped before the created event (e.g. during tool/MCP init), the
final handler restores requestMessage.text to the draft, but the pending-quote
atom was already drained on submit — so a retry sent no quotes. The abort
requestMessage now carries quotes (preliminary metadata + abort fixes), so the
three early-abort/no-response draft-restore paths in useEventHandlers now also
re-queue pendingQuotesByConvoId from requestMessage.quotes.

*  fix: Use Ariakit Popover for quote selections (keyboard focus)

The multi-selection popup used a hand-rolled Radix Popover with Popover.Anchor +
a manual button, so Radix had no trigger to return focus to — Escape dumped
focus to the page top. Refactored to Ariakit (the codebase's popover primitive,
per DropdownPopup/Fork): the `PopoverDisclosure` is the real trigger, so Escape
closes and returns focus to the composer instead of the top of the page. Keyboard
opens (Enter/Space) autofocus into the list and tab through each excerpt's remove;
hover opens for mouse with autofocus suppressed so it never pulls focus off the
composer. e2e asserts the keyboard open/navigate/Escape flow keeps focus on a
real control (never BODY).
2026-06-21 08:33:11 -04:00
Danny Avila
e515063ffe
🔗 feat: Snapshot Files for Shared-Link Attachments (#13740)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🔗 feat: Snapshot Files for Shared-Link Attachments

Shared-link viewers could read a shared conversation snapshot but not its
attachments: file preview/download still went through the owner-scoped file
ACL (the /api/files router sits behind requireJwtAuth + owner/agent checks),
so anonymous viewers got 401s and authenticated non-owners got 403s — the
repeated `[fileAccess] denied` warnings seen for the preview poller.

Capture an immutable per-share file snapshot (embedded on the SharedLink
document, referencing the original stored object — no byte copy) at share
create/update, and serve those files through new share-scoped routes
authorized by the existing shared-link view permission (public/ACL) plus
snapshot membership, never the owner's live file ACL.

- data-schemas: fileSnapshots on the share doc; capture in create/update;
  read-time rewrite of filepath/preview to /api/share/:id/files/:fileId;
  getSharedLinkFile + lazy backfillSharedLinkFiles for legacy links
- api: GET /api/share/:shareId/files/:file_id[/download|/preview]; route
  context added to fileAccess denial logs
- packages/api: isFileSnapshotEnabled resolver (env + yaml)
- data-provider: interface.sharedLinks.snapshotFiles (default on) + client
  endpoints/services
- client: ShareContext.shareId wired to Image, preview hook, and downloads
- config: SHARED_LINKS_SNAPSHOT_FILES env override (default on)

* 🔒 fix: Address Codex review on shared-link file snapshots

Triage of the Codex review on PR #13740 (2 P1, 7 P2 — all valid):

- P1 (cross-user access): scope the snapshot lookup to the sharing user's own
  files so a message referencing another user's file_id can't widen access.
- P1 (stored XSS): the inline share-file route now serves only safe preview
  types inline (raster images/pdf); everything else is forced to attachment with
  X-Content-Type-Options: nosniff.
- Stream shared downloads by default; redirect to a signed URL only on
  ?direct=true (blob/XHR callers work without bucket CORS).
- Read preview status live from the file record (always current for deferred
  previews) and stop embedding extracted text in the share doc (16MB-limit risk).
- Only lazily backfill when the fileSnapshots field is absent (legacy), not on
  every snapshot miss.
- Backfill legacy shares before rewriting message URLs, and gate URL rewriting
  to public shares so non-public (ACL) shares keep prior behavior (img/anchor
  can't carry the bearer token).
- Frontend: only route a download through the share path when the file was
  actually snapshotted (rewritten href / filepath), else fall back.

* 🔑 feat: Authorize shared-link files for non-public shares via cookie

Extends shared-link file access to non-public (ACL) shares (Codex finding 5).
`<img>`/anchor requests can't carry the bearer access token, so non-public
shares previously 401'd on file loads. Add an optional cookie-auth fallback on
the share file routes that resolves the viewer from the `refreshToken` cookie
(or signed `openid_user_id` cookie) — the same mechanism secure image links use
(validateImageRequest) — then let canAccessSharedLink run the viewer's ACL check.

- new middleware optionalShareFileAuth (+ unit spec); applied to the three
  share file routes after optionalJwtAuth
- URL rewriting in getSharedMessages is no longer gated to public shares (the
  route now authorizes header-less requests), so files work uniformly across
  public and non-public shares; revert the now-unused req.sharePublic plumbing

* 🔒 fix: Second Codex pass on shared-link file snapshots

Addresses the follow-up Codex findings on PR #13740:

- Don't snapshot transient text-source files: FileSources.text filepaths are
  Multer temp paths the upload route deletes, so they can't be streamed —
  removed from the streamable allowlist.
- Unset stale snapshots on a disabled-feature update: updateSharedLink now
  $unsets fileSnapshots when snapshotFiles is false, so an opted-out update
  can't keep serving file ids the update dropped.
- Load tenant config after share resolution: configMiddleware now runs after
  canAccessSharedLink (which enters the share's tenant ALS context), so
  per-tenant interface.sharedLinks.snapshotFiles overrides apply to anonymous
  public views.
- Return a clean 404 when the snapshotted object is gone: resolveShareFile now
  requires the live file record and 404s if it's been deleted/expired, instead
  of letting the stream error after headers are sent (ENOENT / 500).

(The re-flagged P1 about private-viewer rewriting was already fixed in the prior
commit's cookie-auth change.)

* 🔒 fix: Third Codex pass on shared-link file snapshots

Addresses the third Codex review pass on PR #13740:

- P1: keep shared previews/files pinned to the snapshotted version. Snapshot the
  small previewRevision; resolveShareFile 404s when the live file's revision no
  longer matches (file_id reused/overwritten by a later turn), so old links can't
  surface post-share content — covers both preview text and streamed bytes.
- Honor the toggle as a kill switch: resolveShareFile 404s when snapshotFiles is
  disabled, instead of only skipping backfill, so disabling stops serving
  already-snapshotted file URLs.
- Lazy-sweep orphaned 'pending' previews to 'failed' in the share preview route
  (mirrors the owner route) so the client poller reaches a terminal state.
- Resolve the cookie-fallback user in runAsSystem so strict tenant isolation
  doesn't throw before canAccessSharedLink establishes the share tenant context.

*  feat: Per-link "share files" checkbox for shared links

Add a checkbox to the share-link dialog (checked by default) letting the user
choose whether to include the conversation's files in the shared link, with
copy explaining images/files won't be visible to viewers otherwise. Opting out
skips snapshot creation/serving for that link.

- client: ShareButton renders the checkbox gated on the new
  startupConfig.sharedLinksSnapshotFilesEnabled flag; state threads through
  SharedLinkButton into the create/update mutations as `snapshotFiles`.
- data-provider: createSharedLink/updateSharedLink send `snapshotFiles` in the
  body; TStartupConfig gains `sharedLinksSnapshotFilesEnabled`.
- api: POST/PATCH /api/share compute snapshotFiles as
  isFileSnapshotEnabled(req.config) && body.snapshotFiles !== false (admin gate
  AND per-link opt-out); config.js exposes the effective enabled flag to clients.
- en locale: com_ui_share_files (+ _description).

* 🐛 fix: Make the "share files" opt-out actually hide files

Unchecking "share files" at creation didn't hide anything: the shared message
JSON still carried each file's original (e.g. static-served) path, and because
opting out only meant "no fileSnapshots field" — indistinguishable from a legacy
link — getSharedMessages would backfill snapshots on first view whenever the
admin feature was on, re-enabling files entirely.

Fix by persisting and honoring the per-link choice:
- Store `snapshotFiles` (boolean) on the SharedLink so opt-out is distinct from a
  legacy link; set it on create and update.
- getSharedMessages computes includeFiles = adminEnabled && link not opted out;
  when excluded it strips files/attachments from the payload (no original-path
  leak) and never backfills the opted-out link.
- Surface the stored choice via getSharedLink so the dialog checkbox reflects an
  existing link's actual setting instead of always defaulting to checked.

Note: changing the checkbox on an already-created link still applies only when
the link is refreshed (which regenerates the URL) — a UX follow-up.

* 🔒 fix: Close remaining shared-link file opt-out leaks (Codex)

Follow-up to the per-link opt-out, addressing the third Codex pass:

- Honor the opt-out on the file route too: getSharedLinkFile now returns the
  link's `optedOut` choice; resolveShareFile 404s (and never backfills) an
  opted-out link, so a direct /files/:id request can't re-create snapshots.
- Make read/serve viewer-independent: the gate no longer uses the viewer's
  resolved config (isFileSnapshotEnabled(req.config)) — it uses the link's stored
  choice plus a global env-only kill switch (isFileSnapshotKillSwitchActive). A
  viewer's own interface.sharedLinks.snapshotFiles can no longer hide a link's
  files. Create/update still use the creator's config to set the per-link choice.
- Neutralize render URLs for non-snapshotted files: applyShareFileRoute now
  strips filepath/preview for any file/attachment not in the snapshot, so the
  owner's original (e.g. static) path can't be loaded through the share.

* 🔒 fix: Harden shared-file version pinning and local path handling (Codex)

- Refuse reused/overwritten file snapshots more broadly: resolveShareFile now
  refuses to serve when either previewRevision OR `bytes` changed vs the
  snapshot. `bytes` catches non-office reused outputs (e.g. code-exec
  same-filename images that lack previewRevision) and is stable across S3 URL
  refresh and the pending->ready transition. Same-size content swaps remain a
  best-effort gap inherent to the no-byte-copy design.
- Strip cache-busting query strings before local streaming: code-output images
  add `?v=...` to filepath; the share route now splits it off so getLocalFileStream
  resolves the real filename instead of a literal `*.png?v=...` path.

* 💬 fix: Clarify that file-sharing changes apply on link refresh

For an already-created shared link, changing the "share files" checkbox only
takes effect when the link is refreshed (which regenerates the snapshot). Add a
note under the checkbox, shown only when a link already exists, so the behavior
isn't surprising: "Refresh the link to apply this change — files are snapshotted
when the link is refreshed."
2026-06-20 23:05:13 -04:00
Danny Avila
c9180d1ad6
🎯 fix: Narrow Public Share 401 Bypass to the Share Endpoint Only (#12905)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
2026-06-20 13:57:21 -04:00
Danny Avila
f76a5faa9e
📌 feat: Seed Default Pinned Tools and MCP Dropdown via Interface Config (#13865)
*  feat: Add `defaultPinnedTools` interface config for default tool & MCP pinning

Adds an `interface.defaultPinnedTools` string array letting admins pin tools and the MCP servers dropdown to the prompt bar by default for all users.

- Tool keys (artifacts, execute_code, web_search, file_search, skills) pin their badge via `useToolToggle`.

- The keyword `'mcp'` or a configured MCP server name pins the MCP dropdown via `useMCPSelect`.

- Only seeds initial state; a user's stored pin preference always wins. When unset, tools start unpinned and the MCP dropdown keeps its legacy default (pinned).

Unifies the approaches from #11646 (pinnedTools) and #9251 (defaultPinMcp) into one config key.

* 🐛 fix: Apply defaultPinnedTools pin once startupConfig resolves

On a cold load, useToolToggle can mount before useGetStartupConfig() resolves, so defaultPinned starts false and useLocalStorageAlt eagerly persists it; its init effect never re-runs for the later config-driven default. Fresh users would then miss the admin-configured default pin whenever startup config was not already cached.

Capture whether a pin preference existed before mount (pre-seed) and, once startupConfig arrives, apply the real default for users with no prior preference. Runs once and never overrides an existing stored pin, so the conservative behavior for existing users is preserved.

* 🐛 fix: Preserve pin clicks made before startupConfig resolves

The cold-load default-seeding effect captured the stored-pin state only at mount, so a pin toggled before startupConfig resolved was treated as no-preference and overwritten when the admin default applied.

Track explicit pin toggles via a ref (set through the returned setter) and skip the default application when the user has interacted in-session — in addition to the existing stored-preference guard.
2026-06-20 13:40:10 -04:00
Tómas Pálsson
229c54c843
🪢 fix: Paginate MCP tools/list to Load All Tools (#13840)
* 🪢 fix: Paginate MCP tools/list to load all tools

MCP `tools/list` is cursor-paginated, but LibreChat only ever read the
first page. `MCPConnection.fetchTools()` called `client.listTools()` once
and discarded `nextCursor`, and `MCPServerInspector` — which builds the
agent-facing tool registry at startup and per request — called the raw
`client.listTools()` directly. Servers that paginate (e.g. an aggregating
gateway exposing hundreds of tools) only ever exposed page one; tools on
later pages were never registered, and invoking one returned
"This tool's MCP server is temporarily unavailable."

- `MCPConnection.fetchTools()` now follows `nextCursor` across pages and
  concatenates every page's tools, bounded by a configurable page cap
  (`MCP_TOOLS_LIST_MAX_PAGES`, default 50) and a repeated-cursor guard so a
  misbehaving server cannot loop forever. Tools already fetched are returned
  if a later page fails, and the no-throw error contract is unchanged.
- `MCPServerInspector.getToolFunctions()` and `fetchServerCapabilities()`
  now route through `fetchTools()`, so the canonical startup and
  per-request tool registry is fully paginated too.

* style: Sort MCP test imports

* style: Sort mutation type imports

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-06-20 11:04:06 -04:00
Danny Avila
36ae268620
🐛 fix: resolve dayjs plugin ESM imports in data-provider (#13851)
`parsers.ts` imported `dayjs/plugin/utc` and `dayjs/plugin/timezone`
without a file extension. The tsdown build externalizes all bare imports,
so it emitted those specifiers verbatim into `dist/index.mjs`. dayjs@1.11
ships no `exports` map, so under strict Node ESM the extensionless
subpaths fail with ERR_MODULE_NOT_FOUND ("Did you mean to import
'dayjs/plugin/utc.js'?"), breaking every strict-ESM consumer that
transitively imports data-provider (and data-schemas, which re-exports
it) — e.g. vitest suites in downstream apps.

Add the `.js` extension so the externalized imports resolve. With
moduleResolution: bundler the types still resolve from the plugin
`.d.ts`. Bump to 0.8.507 to supersede the broken 0.8.506 publish.

Verified: build clean, `dist/index.mjs` imports under strict Node ESM
(node --input-type=module), parsers specs 50/50.
2026-06-19 11:11:08 -04:00
Danny Avila
91f25b8302
📦 chore: bump @librechat/agents to v3.2.42 (#13848)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* 🔧 chore: Update dependencies in package-lock.json and package.json

Bump `form-data` to version 4.0.6 and update `hasown` and `mime-types` dependencies in package-lock.json. Add an `overrides` section in package.json to ensure compatibility with the new `form-data` version.

* 📦 chore: Bump `@librechat/agents` to v3.2.42
2026-06-19 09:47:46 -04:00
Danny Avila
2c6db6bf73
🪞 fix: Match Prompt Cache TTL Control to Region Combobox Styling (#13839)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
The Prompt Cache Duration control used `component: 'dropdown'`
(DynamicDropdown → SelectDropDown), which renders with a different
trigger style than the sibling Region selector and expands its options
inline (headless-ui Listbox) — causing a visual mismatch and layout
shift in the parameters panel.

Switch both the Anthropic and Bedrock promptCacheTtl definitions to
`component: 'combobox'` (DynamicCombobox → ControlCombobox), the same
component Region uses: matching rounded trigger styling, an Ariakit
popover (portal, no layout shift), and — as a bonus — it persists
selections via setOption on custom endpoints (DynamicDropdown's custom
branch is a no-op TODO). Renames the placeholder fields to the
combobox's selectPlaceholder/selectPlaceholderCode.
2026-06-18 22:25:54 -04:00
Danny Avila
268fcbb78d
🕐 feat: Add promptCacheTtl model parameter for 1h/5m cache duration (#13835)
Some checks failed
Publish `librechat-data-provider` to NPM / pack (push) Waiting to run
Publish `librechat-data-provider` to NPM / publish-npm (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
Publish `@librechat/data-schemas` to NPM / pack (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
* 🕐 feat: Add promptCacheTtl model parameter for 1h/5m cache duration

Adds a user-configurable `promptCacheTtl` parameter (dropdown: 5m | 1h)
alongside the existing `promptCache` toggle for Anthropic, Bedrock, and
OpenRouter endpoints. Default is undefined so the agents SDK applies its
own default (1h), letting users opt down to the legacy 5m TTL.

- data-provider: schema, parameterSettings dropdown, types, bedrock picks
- data-schemas: convo/preset types + mongoose defaults
- api: thread promptCacheTtl into anthropic + openai(OpenRouter) llmConfig
- i18n: en translation keys for label/description/default placeholder
- tests: anthropic llm.spec coverage for set + unset cases

* 🔧 fix: Tie Bedrock promptCacheTtl to promptCache + thread OpenRouter TTL params (Codex review)

- bedrock.ts: clear promptCacheTtl whenever promptCache is off/unsupported,
  so an unsupported 1h is never sent on a non-caching Bedrock request
- openai/llm.ts: resolve promptCacheTtl through the same defaultParams/
  addParams/dropParams machinery as promptCache (via promptCacheTtlValue)
  so OpenRouter custom endpoints can configure/override/drop it
- tests: bedrock TTL-tied-to-promptCache cases; OpenRouter TTL default/add/drop

* 🎨 style: Sort imports in openai/llm.spec.ts (CI sort-imports)

*  test: Prove OpenRouter TTL-only selection honors promptCache default (Codex review)

OPENROUTER_DEFAULT_PARAMS injects promptCache:true into defaultParams, so a
TTL-only dropdown selection (promptCacheTtl set, promptCache switch untouched)
still resolves caching on and forwards the TTL. Add regression tests via the
real getOpenAIConfig entry point: TTL-only -> promptCache+TTL both set;
explicit promptCache:false -> both dropped.

* 🔖 chore: Bump librechat-data-provider to 0.8.506

* 🔧 fix: Drop Anthropic promptCacheTtl when promptCache is dropped (Codex review)

dropParams: ['promptCache'] deleted requestOptions.promptCache but left
promptCacheTtl behind, so the admin opt-out path could still carry a TTL
on a request with caching disabled. Clear the TTL alongside promptCache.
2026-06-18 16:36:43 -04:00
Marco Beretta
d8474864e9
🕰️ feat: Resolve Agent Prompt Time Variables in User's Timezone (#13815)
Server-side resolution of {{current_date}} and {{current_datetime}} for
agent instructions used the server's timezone, so agents received UTC
instead of the user's local time the variables are documented to provide.

The browser's IANA timezone is now sent with each request and threaded
through replaceSpecialVars, anchoring those variables to the user's local
wall clock. {{iso_datetime}} stays UTC. Invalid or missing zones fall back
to the previous behavior.
2026-06-18 08:39:56 -04:00
Danny Avila
58647bc08b
🔖 fix: Decrement Bookmark Counts When Deleting Conversations (#13830)
* 🔖 fix: Decrement Bookmark Counts When Deleting Conversations

Deleting a bookmarked/tagged conversation removed the conversation but never decremented the affected ConversationTag counts, leaving stale bookmark counts in the UI.

- Add decrementTagCounts helper that atomically decrements tag counts (clamped at 0, deduped per conversation) in deleteConvos, covering single delete, clear-all, and account deletion.
- Invalidate the conversationTags query in the single-delete and clear-all client mutations so counts refetch.
- Add deleteConvos tag-count tests.

* 🔒 fix: Guard tag-count decrement on actual deletion and message-failure

Addresses Codex review findings:
- Guard the decrement on deleteConvoResult.deletedCount > 0 so a losing concurrent delete (double-click/two-tab) does not decrement counts for a conversation it did not actually remove.
- Move the count adjustment to run immediately after the conversation deletion, before message cleanup, so a deleteMessages failure cannot leave bookmark counts permanently stale.
- Add regression tests for both cases.

* 🔀 fix: Refresh project stats after message cleanup in deleteConvos

Addresses Codex finding: bundling refreshChatProjectStatsForUser into a Promise.all before deleteMessages let a stats-refresh error abort the function and orphan the deleted conversations' messages. Split the steps so the (best-effort) tag-count decrement still runs before message cleanup (counts reconciled even if messages fail), while project-stats refresh runs after, matching the original ordering.

*  test: Add e2e coverage for bookmark counts on conversation delete

Two mock-harness specs for the deleteConvos bookmark-count behavior:
- Deleting the only conversation carrying a bookmark drops its count to 0.
- Deleting one of two conversations that share a bookmark leaves the count at 1.
Both assert the persisted server count via GET /api/tags after the real delete round-trip.

* chore: import order
2026-06-18 08:37:08 -04:00
Danny Avila
a6b5343220
📦 chore: npm audit fix (#13828)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Publish `librechat-data-provider` to NPM / pack (push) Waiting to run
Publish `librechat-data-provider` to NPM / publish-npm (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* 🔧 chore: Update `@librechat/agents` to v3.2.38 and bump related dependencies in package-lock.json and package.json files

* 🔧 chore: Upgrade `multer` dependency to version 2.2.0 in package-lock.json and package.json

* 🔧 chore: Upgrade `nodemailer` dependency to version 9.0.1 in package-lock.json and package.json

* 🔧 chore: Upgrade `@aws-sdk/client-bedrock-agent-runtime` and `@aws-sdk/client-bedrock-runtime` to versions 3.1071.0, update related dependencies in package-lock.json and package.json

* 🔧 chore: Upgrade `form-data` to version 4.0.6 and `hono` to version 4.12.25, update related dependencies in package-lock.json and package.json

* 🔧 chore: npm audit fix

* 🔧 chore: Remove unused Babel dependencies from package-lock.json and package.json

* 🔧 chore: Add '@mistralai/mistralai' to esModules in Jest configuration files
2026-06-17 21:54:04 -04:00
Dan Lew
743f57f63e
🔖 feat: Add Pinned Conversations (#13492)
* feat: add `convo.pinned`

We want to be able to pin convos (so users can easily find them), thus we
added a new field to the DB schema: `pinned`.

We also had to add an API method for pinning a convo. It's got thorough tests.
It's structured just like how /api/convos/archive works, only for pinning.

* feat: add 'pinned' section to conversation list

If there are any pinned conversations, they will appear above the normal
"chats" list, with a pinned icon next to them.

* feat: added pin/unpin to convo options

ConvoOptions now has a pin/unpin button which lets you change the
pin status of any given conversation.

* fix: adjust ellipsizing gradient on ConvoLink

Because it went across the whole ConvoLink, it would cover up any
children (i.e. icons) that appear after the title. However, the point
of the gradient is just to gradually make the title disappear, not
the icons.

This change places the gradient on the title only, so it achieves
the same ellipsizing effect without interfering with the display of
the child icons.

* Fixed import sorting
2026-06-17 20:26:55 -04:00
Danny Avila
fdc7e64bb7
🪙 feat: SDK-Aligned Context-Usage Projection (gauge for window-switch & snapshot-less branches) (#13801)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🪙 feat: Context-usage projection — data-provider + client wiring

Consumer side of the SDK-aligned context projection (agents
`projectAgentContextUsage`). Adds the `/api/endpoints/context-projection`
data-provider plumbing (endpoint, service, query key, `TContextProjectionRequest`)
and a `useContextProjectionQuery` gated to fire only when no fresh snapshot
covers the viewed branch.

Wires `useTokenUsage` precedence to: live snapshot → fresh persisted snapshot
(window matches the resolved one) → server projection → per-message estimate.
A model/window switch marks the baked snapshot stale (its `maxContextTokens`
no longer matches) and falls to the projection — closing the gauge's
window-switch (G1) and snapshot-less-branch (G2) gaps. Snapshot and projection
share the render-relevant fields, so they render uniformly.

Backend endpoint + agents version bump land in follow-up commits. Includes the
design spec (CONTEXT_PROJECTION_SPEC.md).

* 🪙 feat: Context-projection backend endpoint

POST /api/endpoints/context-projection → resolveContextProjection (packages/api):
reconstructs the viewed branch (parent-chain walk from messageId), resolves the
agent config (instructions/provider/model/maxContextTokens), reuses LibreChat's
stored per-message tokenCounts as the index map (no re-tokenizing), and calls
the agents SDK projectAgentContextUsage — no model call. Thin controller injects
db.getMessages/db.getAgent; route mirrors /token-config.

First cut targets message-windowing accuracy; tool-schema tokens are deferred to
a follow-up that reuses the full initializeAgent path.

* 🩹 fix: Codex review on context projection (G1 guard, IDOR, recount, summary)

- Guard `currentActive` against a stale window: a model/window switch on the
  current branch left the live snapshot outranking the projection (G1 didn't
  fire). Now defers to the projection unless streaming or the window matches.
- Scope branch lookups to the authenticated user (`getMessages` filter +
  injected `userId`) — was loading any conversation by id (IDOR).
- Recount messages with no stored `tokenCount` via the tokenizer instead of
  charging 0, so snapshot-less/imported histories don't under-report.
- Fall back (null) for already-summarized branches rather than projecting from
  the full raw parent chain (the next call would send summary + tail); the
  client's summary-baseline-aware estimate handles them until a follow-up
  replays the summary boundary.

* 🩹 fix: Codex round 2 — drop agent load, summary marker, edit-invalidation

- Stop loading agent/model-spec config server-side (closes the agent-access
  IDOR and the spec-prompt special-casing). Provider/model/window now come from
  the client-resolved request (`limits.endpoint`/model — the agent's real
  provider, not the `agents` endpoint, so the tokenizer is right). Agent/spec/
  promptPrefix instructions are uniformly deferred to the full-fidelity follow-up.
- Detect summarized branches via the live path's `metadata.summaryUsedTokens`
  marker (was the wrong `summaryTokenCount` field) and fall back to the
  summary-aware estimate.
- Invalidate the projection query on in-place message edits via a branch
  content `revision` in the cache key (the tail id is unchanged on edit).

Deferred (valid, not a regression): same-window endpoint/model switch keeps a
window-matched snapshot — needs endpoint/model persisted on the snapshot, which
lands with the fidelity follow-up. Smoke-tested: fits / prunes / summarized→null
/ no-window→null.

* 🛡️ fix: make context projection strictly additive (no-regression)

Revert the G1 window-match guard on the live/branch snapshot. When no explicit
maxContextTokens is set (the common default), the SDK's snapshot window is
reserve-derived (~0.9·(modelContext − maxOutputTokens)) while useTokenLimits
resolves the raw model context — so `snapshot.maxContextTokens === resolvedMax`
is false for the SAME model, and the guard would wrongly drop a valid
current-branch snapshot to projection/estimate post-stream (a regression in the
default case, per initialize.ts:1240-1243).

The projection now activates ONLY for snapshot-less branches (G2): the
precedence is live snapshot → persisted branch snapshot → projection → estimate,
where the first two are byte-for-byte the prior behavior and the projection just
slots ahead of the estimate. Window/model-switch (G1) detection needs the
snapshot to carry its model/window and defers to the fidelity follow-up.

* 🩹 fix: surface projections as estimates, not authoritative snapshots

A first-cut projection carries the SDK's windowing but omits instruction/tool
overhead, so rendering it as `isEstimate: false` showed a confident under-count
for snapshot-less branches. Mark projection-sourced views `isEstimate: true` +
`snapshotActive: false` (and drop the snapshot field) so they present as a
better estimate than sumBranch — improved used/window number, estimate framing,
no misleading granular breakdown with ~0 tools. Real snapshots stay
authoritative. (Codex round 3, projection.ts:139.)

* 🧹 chore: drop CONTEXT_PROJECTION_SPEC.md from the PR

* 🎨 style: fix import-sort order in projection.ts (CI sort-imports check)

* 🔧 chore: update @librechat/agents dependency to version 3.2.36 in package-lock.json and related package.json files

* chore: npm audit fix

* 🎨 style: fix import-sort order in data-service.ts (CI sort-imports check)

* 🩹 fix: drop dead calibrationRatio in projectionParams (tsc never error)

Inside the ternary, branchSnapshot is narrowed to null (the gate is
), so  accessed a
property on  (frontend typecheck failure). It was also dead — there is
never a snapshot to seed from in this branch — so just remove it.

* Revert "chore: npm audit fix"

This reverts commit 4cdb862d0c.
2026-06-16 17:54:13 -04:00
Danny Avila
4cb35945dc
🩹 fix: Bill Anthropic Prompt-Cache Tokens Once (#13798)
The installed @librechat/agents folds cache_creation + cache_read into
Anthropic usage_metadata.input_tokens (cache-inclusive), but
cacheSubsetProviders omitted anthropic, so splitUsage() took the additive
branch and billed cache tokens twice — at the full input rate and again at
the cache write/read rate. Verified live: a cache-read-heavy Sonnet call was
overcharged 10.7x.

Add Providers.ANTHROPIC to cacheSubsetProviders (single source of truth for
backend billing and client usage normalization). Bedrock stays additive: its
Converse path passes AWS inputTokens through unmodified. Update the Anthropic
regression tests to production-accurate cache-inclusive fixtures.

Fixes #13795
2026-06-16 14:28:48 -04:00
Danny Avila
d0f659fa75
🗜️ fix: Support Windows ZIP MIME Uploads (#13794) 2026-06-16 11:19:06 -04:00
Danny Avila
d18d62e7c1
🪙 refactor: Reconcile Context Gauge to Actual Provider Tokens (#13780)
* 🪙 fix: Reconcile Context Gauge to Actual Provider Tokens

The context gauge could read several× too high (e.g. 213K when the real prompt
was 56K) and stay there across reloads. Root cause: the SDK's calibrationRatio is
`cumulativeProviderReported / cumulativeRawSent`, but a provider's server-side
web search injects large fetched content into the prompt that the SDK never sent
or counted — pinning the ratio at its cap (5) and multiplying every later message
estimate, including post-summary ones. The gauge rendered (and persisted) that
inflated estimate, never the provider's actual token count.

Fix: reconcile the snapshot to the call's ACTUAL prompt tokens (input + cache),
which already arrive in on_token_usage. Only messageTokens is calibration-scaled
(instructions/summary are raw tiktoken), so keep those and set messageTokens to
the remainder, recomputing free space. Shared `promptTokensFromUsage` +
`reconcileContextUsage` in data-provider; applied server-side in
buildPersistedContextUsage (reload-stable) and client-side in useUsageHandler on
each primary usage (corrects at turn-end, no follow-up needed). Also drop the
summary double-count from the Breakdown Messages row.

Deferred (separate agents PR): the SDK over-calibration also fires summarization
prematurely; fixing it needs decoupling real-content estimation from server-side
injection headroom without weakening pruning-overflow safety.

* 🪙 fix: Harden Token Reconciliation for Provider-less + Resume Paths

Codex review on the reconciliation:
- promptTokensFromUsage: when the provider is absent (custom/OpenAI-compatible
  payloads), fall back to the same magnitude heuristic normalizeUsageUnits uses
  (cache ≤ input ⇒ already included) so cached events aren't re-inflated.
- Resume: backfillUsage restores a primary call's usage without replaying a live
  on_token_usage (Redis mode), so the live reconcile never ran and a reconnected
  session stayed on the inflated estimate. New reconcileBackfill reconciles the
  restored snapshot from the final primary call after contextHandler installs it.

* 🪙 fix: Reconcile Resume Snapshot Server-Side, Not via Backfill

Codex: the client reconcileBackfill scanned the resumed run's collectedUsage and
applied the final primary to the latest snapshot — but on a mid-call resume that
usage belongs to an EARLIER call, corrupting the restored gauge.

Move the resume reconciliation server-side: GenerationJobManager.persistTokenUsage
reconciles the stored contextUsage to a primary usage's actual prompt tokens as it
arrives. That usage is the post-invoke truth for the call the latest stored
snapshot precedes (no snapshot is captured between a call's pre-invoke dispatch
and its usage), so it's correct by construction and run-matched. A mid-call resume
(no usage yet) keeps the raw snapshot instead of mis-applying an earlier call's
tokens; it reconciles once the call completes. Removed client reconcileBackfill;
the live-path reconcile (non-resume) stays.

* 🪙 fix: Guard Reconciliation Against Replays and Snapshot Races

Two Codex concurrency findings on the reconciliation:
- Client: reconcile only on a NEWLY folded primary usage. A replayed duplicate
  (folded=false on resume) can be an earlier tool-loop call sharing the run id,
  which would overwrite the latest snapshot with an earlier, smaller prompt. Moved
  the reconcile after the folded guard.
- Server: serialize the context-usage write through the same per-stream queue as
  the token-usage write. persistTokenUsage reconciles the stored snapshot
  (read-modify-write); an unserialized trackContextUsage could store a newer
  snapshot between the read and write — or a stale reconciled write could land
  after a newer snapshot — clobbering the newer run's gauge when calls interleave.
  FIFO keeps each call's snapshot ahead of its own usage and behind the next.

* chore: import order in GenerationJobManager.ts
2026-06-16 11:05:44 -04:00
Danny Avila
ec94437854
🧾 refactor: Disable Context Cost By Default (#13768) 2026-06-15 15:13:30 -04:00
Danny Avila
b917e0418b
v0.8.7-rc1 (#13592)
* chore: Bump LibreChat to v0.8.7-rc1

* docs: Sync Chinese README
2026-06-15 13:10:30 -04:00
Danny Avila
9efe4878e7
🅰️ feat: Native Anthropic Provider for Custom Endpoints (#13748)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
* 🅰️ feat: Native Anthropic provider for Custom Endpoints

Let a custom endpoint declare `provider: anthropic` to use the native Anthropic
`/v1/messages` client (the agents SDK's ChatAnthropic) against its own
`baseURL`/`apiKey`/`headers`, instead of being forced through the
OpenAI-compatible client. Enables Anthropic itself and Anthropic-compatible
gateways (AI gateways, OpenCode Zen, etc.) as custom endpoints — including for
agents and role-scoped model access.

Closes #10655 (Option 1: explicit provider).

- Schema: add optional `provider` (currently `anthropic`) to the custom
  `endpointSchema` in data-provider.
- Routing: `getProviderConfig` maps a custom endpoint with `provider: anthropic`
  to `Providers.ANTHROPIC` (was always `Providers.OPENAI`).
- Config: `initializeCustom` builds the native Anthropic config via the Anthropic
  `getLLMConfig` (custom baseURL/apiKey/headers) and returns `provider: anthropic`;
  `useLegacyContent` is left unset to match the built-in Anthropic endpoint. The
  OpenAI-compatible path is unchanged for endpoints without `provider`.
- Summarization: `resolveSummarizationProvider` builds an Anthropic config for a
  cross-endpoint native-Anthropic summarization target (self-summarize already
  reuses the agent's client options).

Title generation already resolves via `agent.endpoint`, and provider-specific
handling (tool conflicts, content/PDF validation, token counting, streamUsage)
already branches on `Providers.ANTHROPIC`, so it applies automatically.

Note: model auto-fetch (`models.fetch`) uses the OpenAI `/models` convention and
is not used for this provider — list models explicitly under `models.default`.

* 🅰️ fix: Anthropic custom-endpoint param parity (Codex review)

Address Codex P2 findings — the native Anthropic path must match the
OpenAI-compatible path's parameter handling:

- UI param set: `loadCustomEndpointsConfig` now surfaces `provider` as the
  client `customParams.defaultParamsEndpoint`, so the Agents model panel shows
  Anthropic fields (`maxOutputTokens`/`thinking`) instead of OpenAI `max_tokens`
  (which the native initializer ignored). An explicit non-default
  `defaultParamsEndpoint` still wins.
- Provider override: `getProviderConfig` re-applies `provider: anthropic` after
  all `customEndpointConfig` resolution, so it also wins when the endpoint name
  collides with a known custom provider (e.g. `openrouter`) — fixing the
  token/context budget derived from `overrideProvider`.
- Default params: the native path (and cross-endpoint Anthropic summarization)
  now apply `customParams.paramDefinitions` defaults via `extractDefaultParams`,
  matching what `getOpenAIConfig` does for the OpenAI-compatible path.

Adds tests for each.
2026-06-14 18:23:48 -04:00
Danny Avila
2350ebb24a
📨 feat: Custom Headers on Built-in Provider Endpoints (#13742)
* 📨 feat: Custom Headers on Built-in Provider Endpoints

Add a `headers` config option to the built-in `openAI`, `anthropic`, and
`google` endpoints (incl. Anthropic/Google Vertex), mirroring the custom
endpoint header mechanism. Values support the same placeholder resolution
(env vars, `{{LIBRECHAT_USER_*}}`, `{{LIBRECHAT_BODY_CONVERSATIONID}}`) and
are resolved at request time so dynamic values like conversationId resolve
against the live request — without losing provider-native request shaping.

Closes #13082. Covers #13713: forwarding conversationId to a reverse proxy
is now `X-Conversation-Id: '{{LIBRECHAT_BODY_CONVERSATIONID}}'` — an unknown
header is ignored by the native Anthropic API, so no 400 and no metadata
gating needed.

- Schema: `headers` on `baseEndpointSchema` (openAI/google/anthropic/all).
- New `mergeHeaders`/`resolveConfigHeaders` utils centralize the per-provider
  header locations (`configuration.defaultHeaders`, Anthropic
  `clientOptions.defaultHeaders`, Google `customHeaders`); provider-managed
  headers (auth, `anthropic-beta`) always win on collision.
- Each initializer threads configured headers (endpoint over `all`) into the
  right place; request-time resolution runs across all locations in the main
  and title flows.

* 🩹 fix: Cast endpoints.all to TEndpoint for headers DeepPartial widening

Adding `headers` (a Record) to `baseEndpointSchema` makes `DeepPartial<TCustomConfig>`
widen its value type to `string | undefined`, which is not assignable to the
concrete `TEndpoint['headers']: Record<string, string>` at the `loadedEndpoints.all`
assignment. Cast at the assignment site, mirroring the existing
`anthropicConfig as TAnthropicEndpoint` cast in the same function.

* 🛡️ fix: Harden built-in endpoint custom headers (Codex review)

Address Codex P2 findings on the custom-headers feature:

- Anthropic title requests: `omitTitleOptions` strips the `clientOptions`
  carrier, which dropped its `defaultHeaders`. Preserve just the header carrier
  so gateway/reverse-proxy metadata still reaches title generation.
- mergeHeaders: match header names case-insensitively so an override (e.g. a
  provider-managed `Authorization`/`anthropic-beta`) replaces/uniones a
  case-variant from the base instead of emitting two names a client may collapse.
- OpenAI: withhold admin-configured headers when the user supplies the base URL
  (`user_provided`), since values may carry `${SECRET}`/token placeholders that
  must not reach a user-controlled endpoint — mirrors the custom-endpoint guard.
- Azure: honor global `endpoints.all` headers (same OpenAI carrier) while keeping
  Azure-managed `api-key`/version headers authoritative.

Adds tests for each.

* 🔐 fix: Resolve-once + provider-managed header safety (Codex review round 2)

Address Codex P2 findings:

- Azure: keep global `endpoints.all` headers unresolved at init and let
  request-time `resolveConfigHeaders` resolve them once, avoiding a
  second-order env expansion of already-substituted user values.
- Google: `resolveConfigHeaders` no longer template-resolves the
  provider-managed `Authorization` header (built from a possibly user-provided
  key), so a user key like `${ENV}` can't leak server environment values.
- Model fetches: thread configured headers (endpoint over `all`) + user object
  through `getOpenAIModels`/`getAnthropicModels` → `fetchModels`, so a
  gateway-fronted built-in provider receives the header on `/models` too. Fixed
  `fetchModels` to merge custom headers for Anthropic instead of overwriting
  them (managed `x-api-key`/version still win).

Adds/updates tests for each.

* 🧯 fix: Header provenance, memory/title coverage, idempotency (Codex round 3)

Address Codex P2 findings, including two regressions from the prior round:

- Google auth (findings 6 & 8): move native Google header resolution to init
  (`initializeGoogle`), resolving admin templates BEFORE the key-derived auth
  header is built. resolveConfigHeaders no longer touches Google `customHeaders`,
  so admin `Authorization` templates resolve again (fixes the round-2 regression)
  while the SDK auth header (possibly a user-provided key) is never env-expanded.
- Memory runs: memory extraction now calls `resolveConfigHeaders`, so native
  Anthropic (and OpenAI) headers resolve for memory requests too.
- Vertex titles: restore the ORIGINAL `clientOptions` object reference (not a
  copy) when preserving headers across `omitTitleOptions`, so the Vertex
  `createClient` closure and the resolved headers stay on the same object.
- Reuse: `resolveConfigHeaders` is now idempotent (resolve-once per header map),
  preventing a second pass from env-expanding values already substituted with
  user/body data when an agent object flows through buildAgentInput twice.

Adds/updates tests for each.
2026-06-14 17:02:04 -04:00
Danny Avila
7cf2877b45
🪙 feat: Context Gauge UX, Hover Snapshot, Click Breakdown, Currency, Cost-On-By-Default (#13739)
* 🪙 feat: Default Context Cost On + Configurable Display Currency

Flip interface.contextCost to default-on (schema default true, resolved per-field
in loadDefaultInterface so it applies unless an admin explicitly sets false).

Add interface.currency { code, rate }: an ISO-4217 code and a static USD→local
multiplier so non-USD communities (EUR, JPY, CNY, BRL, ZAR, …) can show costs in
their currency. Inner fields are required (no nested defaults) to keep zod
input/output identical; loadDefaultInterface passes it through. Display-only —
model prices stay USD server-side.

* 🪙 feat: Currency-Aware Context Cost Formatting

formatCost(usd, currency?) applies the static rate (usd × rate) and formats via
a cached Intl.NumberFormat keyed by currency code — locale-correct symbol and
per-currency decimals, falling back to USD on a malformed code. The USD default
(code USD, rate 1) is byte-identical to the prior output.

* 💄 feat: Gauge Hover Snapshot, Click-to-Open Breakdown, Hide Until Data

Replace the hover-only HoverCard with: a compact hover snapshot tooltip
("Context 341.7k / 1.0M (34%)" + cost when enabled) via the existing Tooltip
primitive, and a click-opened Ariakit popover for the full breakdown that
dismisses on outside-click/Escape/blur. Gate visibility on usedTokens > 0 so a
fresh, message-less chat shows nothing, with an animate-in fade as the first
tokens land. Thread the display currency into the breakdown + snapshot.

* 🧪 test: Gauge Interaction + Visibility E2E

Switch the breakdown specs from hover to click, and add a test that the gauge is
absent on a new chat, surfaces the snapshot tooltip on hover, opens the breakdown
on click, and dismisses on Escape and outside-click.

* 🪙 fix: Harden Currency Resolution + Layer Breakdown Above Tooltip

Address Codex review on the currency display:
- Unsupported currency code now falls back to USD AND rate 1, so a typo like
  { code: 'EURO', rate: 0.92 } no longer shows a converted amount under a $
  symbol (was $9.20 for a $10 cost; now $10.00).
- A non-finite/negative rate (e.g. a partial admin override that set code before
  rate) falls back to rate 1, so a cost never renders as NaN.
- Fraction digits derive from the currency's own defaults, so zero-decimal
  currencies (JPY) render ¥5, not ¥5.00, and extra sub-unit precision applies
  only to currencies that have minor units. USD output is unchanged.
- Raise the click breakdown popover to z-[200] so it always sits above the
  z-150 hover tooltip when both briefly coexist.

* 🪙 fix: Validate ISO-4217 Codes + Derive Tiny Threshold from Minor Unit

Address Codex review on currency formatting:
- Intl.NumberFormat accepts any well-formed 3-letter code (EUU, RMB) without
  throwing, so the previous construct-based check missed typos/non-ISO codes and
  applied the rate under a bogus label. Validate against Intl.supportedValuesOf
  ('currency') (the ISO-4217 set); unsupported codes fall back to USD + rate 1.
  Codes are normalized to upper-case; graceful fallback if the runtime lacks
  supportedValuesOf.
- The tiny-amount threshold now derives from the currency's minor unit
  (10^-fractionDigits): 0.01 for 2-decimal, 0.001 for 3-decimal (KWD/BHD/JOD),
  1 for zero-decimal — instead of a hard-coded 0.01. Sub-unit precision trims to
  each currency's own scale. USD output unchanged.
2026-06-14 13:38:27 -04:00
Danny Avila
b03b2a0a29
💾 feat: Persist Context Breakdown & Branch/Total Usage Cost (#13734)
* 💾 feat: Persist Context Breakdown & Branch/Total Usage Cost

Persist the granular context breakdown and per-response usage/cost on the
response message metadata, and re-derive branch + total usage/cost from a
per-message index so the popover survives reloads and is branch-aware live.

- Add aggregateEmittedUsage + buildPersistedContextUsage helpers in
  packages/api; capture the latest visible snapshot and every emitted
  on_token_usage payload via contextUsageSink/usageEmitSink.
- Attach metadata.contextUsage (Part A) and metadata.usage (Part B) on the
  agents response message in sendCompletion.
- Carry per-message usage on the token index; add sumTotalUsage/setEntryUsage
  and branch-scoped usage on sumBranch.
- Repurpose the session accumulator into a single in-flight pending holder;
  flush it into the index at finalize; hydrate breakdowns on load.
- Render branch cost with a conditional all-branches total in the breakdown.

* 🧹 chore: Remove orphaned com_ui_session_cost i18n key

* 🩹 fix: Address Codex review — normalize usage server-side, fix reload deltas

- Persist per-event-normalized display units in metadata.usage (TResponseUsage)
  so reloaded mixed-provider turns match the live session; client reads them
  directly instead of re-normalizing with a single stamped provider (P2).
- Persist completedOutputTokens (final call output) on metadata.contextUsage so
  a reloaded multi-call turn adds the post-snapshot delta, not the full
  tokenCount the snapshot already counts (P2).
- buildIndex preserves a prior entry's immutable usage when a rebuilt cache
  message lacks metadata.usage, so a mid-session rebuild (regenerate) keeps a
  sibling branch's flushed cost (fixes the e2e regenerate failure).
- Track costKnown so turns saved with contextCost off don't render $0.00 when
  cost display is later enabled (P3).
- Use an epsilon for the all-branches cost comparison to avoid a spurious total
  row from float summation order (P3).
- Update unit/integration/e2e tests for the new shapes; regenerate e2e asserts
  the all-branches total after reload (deterministic via persisted metadata).

* 🩹 fix: Address Codex round 2 — pending leak, cost coverage, reload delta

- Clear the in-flight pending usage on terminal abort/error (resetLive), so a
  stopped generation's tokens no longer merge into the next response (P2).
- costKnown now means COMPLETE coverage (ANDed): a branch mixing cost-bearing
  and cost-less turns is flagged incomplete and the cost row is hidden rather
  than rendering an under-reported total (P2).
- Drop the tokenCount fallback for completedOutputTokens on reload: only the
  persisted post-snapshot delta is used, so a multi-call turn whose provider
  emitted no usage_metadata no longer double-counts earlier output (P2).
- Update tokens.spec for AND coverage semantics + incomplete-cost case.

* 🩹 fix: Address Codex round 3 — no-usage snapshots, total coverage, provider-less cache

- Skip persisting metadata.contextUsage when the response emitted no primary
  usage event: without a known post-snapshot output the granular gauge would
  undercount the reply on reload, so fall back to the coarse per-message
  estimate instead (P2).
- Gate the all-branches cost row on totalUsage.costKnown so an incomplete total
  (a sibling saved without cost) never renders an under-reported figure (P2).
- aggregateEmittedUsage/finalCallOutputTokens now normalize per-event with the
  client's magnitude fallback (normalizeEventUnits) instead of billing
  splitUsage, so provider-less cached events match live on reload (P2).
- Add backend test for the provider-less cached case.

* 🩹 fix: Address Codex round 4 — abort attribution, complete cost coverage

- aggregateEmittedUsage persists cost only when EVERY call was priced; a partial
  pricing failure now omits cost so the client treats coverage as unknown rather
  than reading an under-reported sum as authoritative (P2).
- finalizeUsage flushes pending into the response entry only when events were
  folded this session (eventCount > 0), so a late/second resumable subscriber
  carrying persisted metadata.usage keeps it instead of being overwritten with
  an empty pending record (P2).
- On user stop, attribute the in-flight pending usage to the partial response
  (new attributePending handler) instead of discarding it in resetLive — the
  stopped reply's billed tokens are kept and still can't leak into the next
  response; resetLive's discard remains for the error path (P2).

* 🐛 fix: Persist branch cost across branch switches via sticky usage history

Branch cost vanished on switching to a sibling branch (until a new turn) — the
cost analog of the granularity bug. buildIndex rebuilds the token index from the
messages cache; a sibling generated this session whose cache message lacks
metadata.usage (and is transiently dropped from the cache during regenerate)
lost its live-flushed usage, so sumBranch found none and the cost row hid.

Fix: a sticky per-response usage map (conversationId → messageId → usage),
written by setEntryUsage and never rebuilt from the cache — the usage counterpart
of snapshotsByAnchorFamily for the breakdown. buildIndex/upsertEntries restore an
entry's usage from it when the message carries none; cleared on convo switch and
migrated with the index. Add unit coverage for the drop-then-readd regression and
an e2e assertion that branch cost survives a branch switch.

* 🐛 fix: Re-index on branch switch so branch cost survives the switch

The sticky usage history alone didn't fix the reported branch-switch cost drop:
on a branch switch no cache `updated` event fires, so the index subscriber never
re-ran, and the post-regenerate rebuild was skipped while `isSubmitting` was
still true — leaving the index stale and missing the now-viewed branch's
response entirely (sticky can only restore entries present in a rebuild).

Re-index from the messages cache on every tail change (created/finalize AND
branch switch), not just while submitting. The cache holds the full message set
at switch time, so the viewed branch's response is re-added and its usage
restored from metadata.usage or the sticky history → sumBranch finds it and the
branch cost renders. Verified locally: the branch-switch e2e now passes (the
cost section shows both the branch row and the all-branches total). Also fixed
that e2e assertion to target a single cost value (strict-mode safe).

* 🩹 fix: Handle stopped-stream usage — reset pending + persist abort metadata

Codex round (stop/abort edges):
- Resumable explicit-stop (intentional SSE close) reset UI state but never
  cleared pendingUsageFamily, so usage folded before the stop leaked into the
  next response in the conversation. Discard pending on intentional close
  (resetLive); a resume re-folds via backfillUsage, so nothing is lost.
- The abort save path (abortMiddleware) persisted the stopped response without
  metadata.usage/contextUsage, so its cost + breakdown vanished on reload.
  Rebuild both from the job's persisted tokenUsage (emitted payloads incl. cost)
  and contextUsage snapshot — parity with the normal sendCompletion path;
  breakdown gated on a primary usage event like buildResponseMetadata.

Deferred (per scope decision): mid-stream branch-switch transiently shows the
streaming branch's pending on the viewed sibling (cosmetic, until finalize).

* 🩹 fix: Persist abort metadata on the real agents route + tighten snapshot gate

Codex round (corrects last round's wrong-path fixes):
- Stopped AGENTS responses are saved by routes/agents/index.js (/chat/abort),
  not abortMiddleware — so last round's metadata fix never ran for them. Moved
  the rollup/snapshot builder into packages/api as buildAbortedResponseMetadata
  (shared, unit-tested) and applied it in BOTH abort save paths, so a stopped
  agent reply keeps its cost + breakdown on reload.
- Persist the breakdown only when the FINAL visible call emitted usage: track a
  per-response snapshot count and require primaryUsageCount >= snapshotCount.
  Previously any earlier primary usage event passed the gate, so a multi-call
  turn whose final call emitted no usage_metadata used an earlier call's output
  as completedOutputTokens (already counted by the latest snapshot) → reload
  over-reported. Now it falls back to the coarse estimate.

Resumable stop pending-reset (prior round, 3cde6fe035) already flows through
clearAllSubmissions → SSE close → the intentional-close handler's resetLive.
Deferred per scope: mid-stream branch-switch pending attribution (tracked).

* 🩹 fix: Abort breakdown over-count + resume re-fold after pending discard

Codex round (on the re-applied abort/snapshot work):
- buildAbortedResponseMetadata now persists ONLY the usage/cost rollup, not the
  context breakdown. The abort path can't tell whether the final call emitted
  usage (the job stores only the latest snapshot, not a count), so persisting
  the breakdown risked reusing an earlier call's output as completedOutputTokens
  (already in the snapshot) → reload over-count. Stopped/incomplete responses
  now fall back to the coarse gauge estimate, which is safe and apt.
- resetLive now also forgets the conversation's folded usage-event identities
  (clearUsageFolded). Discarding pending on a terminal/intentional close left
  the folded keys set, so a later resume's backfillUsage saw the persisted
  events as duplicates and never rebuilt pending — leaving the response's usage
  missing until a full reload. Clearing them lets the resume re-fold.
2026-06-14 10:48:07 -04:00
Danny Avila
db7011d567
📊 feat: Real-Time Context Window & Token Usage Tracking (#13670)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 📊 feat: Real-Time Context Window & Token Usage Tracking

* 🧪 fix: Align Pricing Spec Dep Signatures with TxDeps

* 🩹 fix: Resolve Codex Findings for Context Usage Tracking

* 📊 feat: Granular Tool Token Breakdown with Deferred Splits

* 🧪 test: Cover Session Cost in Mock E2E and Scope Usage Selectors

* 🧪 test: Live Host-Pipeline Usage Verification (Env-Gated)

* 🧪 test: Local Real-Provider Multi-Turn E2E Harness

* 🪙 fix: Keep Tagged Usage Buckets Out of the Live Context Estimate

* 🩹 fix: Scoped Token-Config Fallback and Sequential Visibility for Usage Events

* 🩹 fix: Address Usage Review Findings — Cost Timing, Scoped Caches, Finalized Output

- carry the post-snapshot output estimate into the context snapshot at
  finalize so the gauge keeps the last response after live resets
- accumulate per-rate billable units and price the session cost at
  render, so usage events arriving before the token-config load still
  count once it resolves
- pass user-scoped token-config cache keys through loadConfigModels
  fetches and drop the controller's unscoped fallback to prevent serving
  another user's resolved config
- tag emitted usage events with a per-run seq so resume dedupe never
  drops a distinct call with an identical payload
- admit the static tokenConfig override in the custom endpoint schema so
  it survives zod parsing into req.config

* 🩹 fix: Align Client Usage Accounting with Backend Cost Semantics

- classify cache tokens by provider (shared inputTokensIncludesCache from
  data-provider, consumed by both the backend billing path and the client)
  instead of a magnitude heuristic, so Anthropic/Bedrock turns where cache
  is smaller than uncached input no longer under-bill input
- mirror resolveCompletionTokens on the client so Vertex-style hidden
  thinking tokens are reflected in the Output row and session cost
- prefer endpoint pricing over adapter-provider pricing so a custom
  endpoint can price a known model name without built-in rates shadowing it
- carry static cacheRead/cacheWrite overrides through the tokenConfig
  schema and buildTokenConfigMap

* 🩹 fix: Honor Static Token Config in Billing; Tighten Usage Freshness

- initializeCustom now uses a static endpoint tokenConfig as the agent's
  endpointTokenConfig (billing + balance checks), not just the advertised
  UI config — previously the gauge showed admin rates while the agent
  billed against built-in tables
- invalidate the token-config query alongside models on user-key add/
  revoke so context windows and pricing refresh without a reload
- include maxContextTokens in ChatForm's stabilized conversation memo so
  the gauge reflects a changed context-window setting immediately
- feed the live output estimate from the legacy content path (direct and
  assistants streams), setting from cumulative part text rather than
  accumulating deltas

* 🩹 fix: Resume Usage Dedup, Agent Pricing, and Partial Override Billing

- fold usage events idempotently by (runId, seq) so resume backfill no
  longer resets the conversation totals — a mid-stream reconnect keeps the
  usage of prompts already completed earlier in the session
- tap replayed pending message/reasoning/content events so output streamed
  past the resume snapshot reaches the live estimate, not just the message
- resolve cost against the agent's backing endpoint (Agents conversations
  report endpoint `agents` / provider `openAI`, neither of which keys a
  custom endpoint's tokenConfig)
- getMultiplier/getCacheMultiplier fall back to the standard tables for
  models absent from a partial endpointTokenConfig, so a partial static
  override no longer bills non-listed models at defaultRate while the UI
  shows the correct pattern rate

* 🩹 fix: Repaired Output in Gauge, Cache-Rate Keys, Config Gate, Usage Cleanup

- live/completed gauge counts the repaired completion (normalized output),
  so under-reporting providers don't drop the response from used context
- translate static tokenConfig cacheWrite/cacheRead onto the write/read
  keys getCacheMultiplier reads, so cache tokens bill at the configured
  rate instead of the prompt-rate fallback
- clear the token index and usage atoms when leaving a conversation, so
  visited histories don't accumulate in memory for the tab's lifetime
- wait for startupConfig before mounting the gauge, so a deployment with
  contextUsage disabled never briefly mounts it or fires the token-config
  query on first load

* 🩹 fix: Move Token-Config Resolution to TS; Key Live Usage by Created Convo

- extract the token-config resolution (override gathering + cache lookup +
  buildTokenConfigMap) into resolveTokenConfigMap in packages/api, leaving
  the /api controller a thin request-scoped wrapper (CLAUDE.md TS rule)
- getConvoKey prefers the user message's real conversationId once the
  `created` event stamps it, so a new chat's first-response live gauge and
  totals land under the id TokenUsage subscribes to instead of NEW_CONVO

* 🩹 fix: Clear Stale Redis Job Usage; Live-Tap Legacy Streams; Share Fetched Config

- DEL the Redis job hash before re-creating it so a reused streamId can't
  inherit a prior run's contextUsage/tokenUsage and backfill stale usage
- tap the legacy {message,text} stream branch (non-agent OpenAI/Anthropic
  streams) into the live estimate, not just the content path
- copy a deduped fetch's token config to every sibling endpoint sharing the
  baseURL/key/headers, so /token-config resolves each by its own name

*  revert: Don't DEL Redis job hash in createJob (breaks cross-replica resume)

createJob is an idempotent join — a second replica calls it for the same
streamId to share an in-flight stream's state. DELeting the hash wiped the
prior replica's persisted created/usage state, so a joining replica missed
the created event (GenerationJobManager cross-replica integration test).
Reverts the F1 change from 2bfce0c34b; the stale-usage concern doesn't
arise in practice (streamId is unique per generation).

* 🩹 fix: Best-Effort Usage Emit; Tag Hidden Sequential-Agent Usage

- wrap the ModelEndHandler usage emit in try/catch so a failed telemetry
  delivery (closed SSE / Redis publish error) can't abort the handler
  before thought-signature capture, which would break resumed tool calls
- tag hidden sequential-agent usage as 'sequential' (non-primary) so the
  client folds it into session cost/totals but not the live context gauge,
  instead of letting an undefined usage_type inflate the visible gauge

* 🩹 fix: Refetch Stale Token Config on Mount; Normalize Vertex for Lookup

- useTokenConfigQuery refetches on mount when stale, so a user-key change
  that invalidates tokenConfig while the gauge is unmounted takes effect on
  return instead of serving the prior key's resolved config
- normalize a Vertex-backed agent's provider (vertexai) to the google
  token-config key, so Gemini context windows and rates resolve instead of
  showing unknown context / $0 cost

*  feat: Server-Side Per-Event Cost (Authoritative Pricing for the Gauge)

Move usage-cost pricing to the single source of truth. The backend prices
each model call with the same billing functions (premium tiers via
getMultiplier(inputTokenCount), cache rates) and emits the USD cost on
on_token_usage when interface.contextCost is enabled; the client sums
emitted costs instead of re-deriving from base token-config rates.

- computeUsageCostUSD reuses prepareTokenSpend/prepareStructuredTokenSpend
  so the emitted cost matches what is billed (incl. premium thresholds)
- getDefaultHandlers gains a usageCost pricing context; initialize.js wires
  db.getMultiplier/getCacheMultiplier gated on contextCost (agents path)
- client UsageTotals carries a summed costUSD; retire the client-side rate
  lookups (costFromUnits/calcUsageCost) that drifted from backend pricing
  and produced the provider-keying / cache-key / Vertex / premium findings
- keep normalizeUsageUnits for the displayed token counts; token-config is
  still used for the context-window meter

Fixes the premium-tier session-cost under-report (gpt-5.x / gemini-3.1
above their input thresholds).

* 🩹 fix: Branch-Accurate Usage Snapshot + Clearer Gauge Track Contrast

- re-anchor the context snapshot from the user message to the response
  message at finalize. Regenerating a response branches off a shared user
  message, so anchoring on it made the snapshot read as "active" on both
  branches — switching to the sibling branch showed the wrong (other
  branch's) context. The response message is branch-unique, so sibling
  branches now correctly fall back to their own per-branch totals.
- raise the gauge ring's track/fill contrast (muted track, prominent fill)
  so the used portion reads clearly as a fill-level indicator

* 🩹 fix: Tag Sequential Usage in Billing; Emit Subagent Cost; Reset Live on Resume Errors

- tag hidden sequential-agent usage `usage_type: 'sequential'` on the
  COLLECTED usage (not just the emit), and treat it as non-primary in
  recordCollectedUsage (billed, excluded from the reported output total) so
  hidden intermediate output stops inflating the parent's tokenCount/pruning
- emit on_token_usage from the subagent usage sink (tagged `subagent`, with
  authoritative cost when contextCost is on) so the gauge's session
  cost/totals include billed subagent usage; it stays out of the live meter
- call resetLive on the resumable 404 and max-retry terminal branches so the
  gauge doesn't keep counting stale in-flight tokens after the stream ends

* 🎨 fix: Contrast the Popup Context Bar; Revert Ring Restyle

- raise the popup breakdown's context progressbar contrast (muted
  surface-tertiary track, prominent text-primary fill) — that's the bar the
  contrast feedback was about
- revert the gauge ring restyle (kept its original border-heavy track /
  text-secondary fill); the ring wasn't the element in question

* 🩹 fix: Stop Snapshot Granularity Leaking Across Branches; Revert Tree Memo

- a null-anchor context snapshot was treated as active on every branch,
  leaking one generation's granular breakdown onto sibling branches. Require
  a non-null (response-message) anchor on the viewed branch instead, so
  siblings without a matching snapshot fall back to their own totals.
- revert the buildTree WeakMap memo in messages.ts. buildTree is pure (builds
  from shallow copies) so the memo was behaviorally identical, but it was the
  feature's only change to core branch-navigation selectors — removing it
  matches upstream and rules it out of branch-navigation debugging.

* 🪙 fix: Thread Endpoint Token Config to Agent Billing, Cost, and Context Limits

Custom-endpoint agents resolve an endpointTokenConfig during agent init but
it never reached the AgentClient, so spending, emitted cost, and runtime
max-token resolution all fell back to default rates for those agents.

- Surface options.endpointTokenConfig on the returned InitializedAgent.
- Pass it to the AgentClient (this.options.endpointTokenConfig) so the
  spending path bills at configured rates.
- Thread it through usageCost to computeUsageCostUSD so emitted per-event
  cost matches billing.
- getModelMaxTokens/getModelMaxOutputTokens fall back to the built-in map
  for models absent from a partial override (matches buildTokenConfigMap);
  consolidates the duplicated fallback in pricing.ts.

* 🪙 fix: Preserve Granular Breakdown Across Branch Switches

The granular context breakdown lives only in the live on_context_usage
snapshot — a single per-conversation slot, anchored to the latest response
and overwritten by each generation. Switching to a branch generated earlier
this session lost its tool/skill/system rows and fell back to coarse totals.

Retain each generation's finalized snapshot in a per-conversation map keyed
by its branch-unique response id (snapshotsByAnchorFamily). When the live
snapshot is off the viewed branch, walk the branch tail for its deepest
stored anchor and render that breakdown. Bounded by generation count and
cleared on conversation switch; the live/just-generated path is unchanged.

* 🪙 fix: Harden Resume Seeding and Subagent Usage Emission

- useResumableSSE: skip the trailing-output live seed when the resume
  carries a context snapshot; the snapshot's messageTokens already counts
  produced output, so seeding it again inflated usage until the next reset.
- AgentClient subagent emitter: await GenerationJobManager.emitChunk like
  every other caller (it persists before publishing), so a floating promise
  can't race job cleanup and a Redis/publish failure is caught by the
  emitter's try/catch instead of surfacing as an unhandled rejection.

* 🧪 test: Playwright Coverage for Context Breakdown Granularity

Add a test-only data-testid distinguishing the granular snapshot breakdown
(context-breakdown) from the coarse message-history estimate
(context-estimate), then assert granularity in the mock e2e harness:

- renders the granular breakdown from the live on_context_usage snapshot
  (guards that the snapshot event actually reaches the popover, not just the
  usage totals).
- preserves the granular breakdown after switching branches — regenerate to
  overwrite the single live snapshot, switch back, and confirm the rows
  survive via the per-anchor snapshot history map.

Branch regenerate/sibling selectors mirror the existing chat.spec branch test.
All three usage specs pass against the mock pipeline.

* 🪙 fix: Correct Resume Live-Seed, Fallback Re-index, and Subagent Emit Flush

Codex round on the prior commit:

- countTrailingOutputChars now counts only output at the very END of the
  aggregated content (0 when the model paused at a tool call), and the resume
  path always seeds it. The earlier skip-trailing-tool-parts behavior plus the
  skip-seed-when-snapshot gate together over- or under-counted in-flight
  output on resume; one rule fixes both — pre-invoke snapshot budget is never
  double-counted, and genuine in-flight output is no longer dropped.
- useTokenUsage re-indexes from the messages cache on tail change while
  submitting. The cache subscriber is muted during streaming, so without a
  context snapshot (non-agent streams) sumBranch missed the created tail and
  dropped history + prompt until finalize. Bounded — tailId only shifts on
  created/finalize/branch-switch.
- AgentClient tracks subagent usage emit promises and flushes them in
  chatCompletion's finally. The sink fires the emitter without awaiting, and
  resume reads the usage emitChunk persists (HSET), so cleanup must not race
  it or resumed clients miss billed subagent usage.
2026-06-13 19:38:28 -04:00
Michael Harvey
05eb986097
💬 feat: Conversation Starters for Model Specs (#13710)
* 💬 feat: Conversation Starters for Model Specs

Adds an optional conversation_starters field to model specs in
librechat.yaml. When the active conversation uses a spec that defines
starters (and no agent/assistant starters apply), the chat landing
renders clickable starter prompts between the landing content and the
chat input; clicking one submits it as the first message.

- data-provider: add conversation_starters to TModelSpec and
  tModelSpecSchema so the field survives strict config parsing
- client: ConversationStarters falls back to the active spec's
  starters via getModelSpec; entity (agent/assistant) starters
  take precedence; starter cards are centered, size to content,
  wrap at word boundaries, stagger their fade-in, and gain a
  focus-visible ring
- sanitizeModelSpecs passes the field through (denylist); covered
  by a new unit test
- e2e: mock spec + tests for rendering, absence, click-to-submit,
  and the MAX_CONVO_STARTERS cap

Closes #3619

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: Sort ChatView imports

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2026-06-13 11:38:49 -04:00
Lacy
dea71c8396
🪟 fix: Cross-Platform Absolute-Path Check in tsdown neverBundle Predicates (#13700)
The deps.neverBundle predicates in the four package tsdown configs detect
first-party (resolved) module ids with !id.startsWith('/'). On Windows,
resolved ids are absolute paths like C:\..., which never match, so every
project module is externalized. Builds still exit 0 but emit near-empty
bundles — e.g. packages/client dist/index.mjs drops from ~276 kB to
~2.7 kB and dist/style.css is never produced, breaking the client dev
server with "Failed to resolve import @librechat/client/style.css".

Replace the startsWith('/') check with path.isAbsolute(id), which is
behavior-identical on POSIX and correct on Windows.

Co-authored-by: phoenixtekk <phoenixtekk@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 11:04:46 -04:00
Danny Avila
197a1dc4e2
🧬 feat: Add GitHub Skill Sync (#13293)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
Publish `librechat-data-provider` to NPM / pack (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
* feat: Add GitHub skill sync

* fix: Address GitHub skill sync CI

* fix: Harden GitHub skill sync review paths

* fix: Prevent overlapping skill sync runs

* fix: Address GitHub skill sync review findings

* fix: Satisfy Git ref lint rule

* fix: Address GitHub sync review follow-ups

* fix: Match skill frontmatter closing fence

* fix: Address GitHub sync review cycle

* fix: Address GitHub sync review follow-ups

* fix: Harden GitHub skill sync worker

* fix: Format GitHub sync rollback log

* fix: Address GitHub sync review feedback

* fix: Format skill import parse handling

* fix: Coerce scalar skill frontmatter and correct scheduler timer clear

- parse: coerce numeric/boolean name and description scalars to strings instead of dropping them to empty (restores pre-refactor behavior; preserves absent-vs-empty distinction for the when-to-use fallback)
- scheduler: clear the setTimeout handle with clearTimeout rather than clearInterval
- test: cover non-string scalar frontmatter coercion

* fix: Tolerate trailing whitespace after SKILL.md opening frontmatter fence

extractFrontmatterBlock required the opening fence to be exactly '---\n', so an opener with trailing spaces/tabs (e.g. '---   \n') silently dropped all frontmatter even though the closing-fence regex already tolerates it. Match the opener with /^---[ \t]*\n/ for symmetry. Addresses Codex P3 (parse.ts:24).

* feat: Run GitHub skill sync under a per-source tenant context

Under TENANT_ISOLATION_STRICT, the sync ran with no async tenant context, so the tenant-isolation mongoose hooks threw on every Skill/SkillFile/AclEntry operation; in non-strict mode synced skills were written tenant-less and never matched tenant-scoped reads. Add an optional per-source tenantId to the skillSync config; when set, each source sync runs inside tenantStorage.run({ tenantId }) so skills, files, and public ACL grants are created and listed within that tenant, and the skill row is stamped with the tenantId for correct dedup. Sources without tenantId keep the prior single-tenant behavior. Avoids runAsSystem. Addresses Codex P2 (sync.js:70).

Lock/status/credential bookkeeping stays outside the tenant context (those collections are intentionally global).

* test: Restore dropped tenant-context coverage for GitHub skill sync

The prior commit shipped the getTenantId import in github.spec.ts without the tenant tests that use it (lost in an interrupted edit), which failed the eslint --max-warnings=0 CI job on an unused import. Restore both github.spec.ts tenant tests (tenant-scoped run stamps tenantId and executes inside the tenant ALS context; no-tenant run stays ambient) and the two config-schemas tenant tests (accepts tenantId, rejects __SYSTEM__).

* test: Restore dropped github.spec tenant-context tests

The previous commit's github.spec.ts edit did not apply (anchor mismatch), so the getTenantId import remained unused and failed eslint --max-warnings=0. Add the two tenant tests that use it: a tenant-scoped run stamps tenantId and executes inside the tenant ALS context, and a no-tenant run stays ambient.

* feat: Scope synced skill author to tenant and harden tenant-context sync

Addresses the latest Codex review on the per-source tenant change:
- makeSourceAuthorId now folds tenantId into the synthetic author hash so the
  same source mirrored into different tenants gets distinct author ids (clearer
  audits, no cross-tenant author collisions). Single-tenant author ids stay
  stable (suffix omitted when tenantId is absent).
- syncSourceInTenantContext uses an async callback per the tenant-context
  contract so the ALS store propagates across awaited Mongoose calls.
- Tests: same-source/different-tenant yields distinct authors; mirror cleanup
  is scoped to the source and deletes only its absent-upstream skills.

* fix: Repair tsc error and guard external edits in github skill sync

- Fix TS2352 in github.spec mirror-cleanup test: build the existing-skill mock via makeSkill with authorName instead of an under-typed 'as CreateSkillInput' cast (this was the failing TypeScript CI check on f00ce3c5a).
- 808: commitExistingRemoteSkillAfterFileSync re-reads to clear our own file-sync version bumps, but now compares refreshed content against the pre-sync snapshot (body/name/description/always-apply) and throws SKILL_CONFLICT on a concurrent external edit instead of overwriting it.

* docs: Note skillSync source tenantId is effectively immutable

Changing/adding/removing a source's tenantId orphans previously mirrored skills in the old tenant (a tenant-scoped sync cannot clean another tenant's data without runAsSystem, which is intentionally avoided).

* fix: Key GitHub skill upstream identity on source id and path only

Addresses Codex finding (github.ts:217): makeUpstreamId previously included owner/repo, so repointing a source to a renamed or replacement repository (same source id) changed the upstreamId, made findSkillBySourceIdentity miss the existing mirror, and then collided on the (name, author, tenantId) uniqueness constraint — leaving the source stuck failing. Identity now keys on the stable source id + root path only. The feature is unreleased, so there is no stored-id migration. Updated spec upstreamId fixtures to the new format; the existing ref-independent identity test now also covers repo moves.

* fix: Scope GitHub skill mirror deletion to the source tenant

Addresses Codex P1 (github.ts:1047/1057): an ambient source (no tenantId) runs listSkillsBySource without tenant context, which under non-strict isolation returns github-synced skills across all tenants. The mirror-deletion pass then treated other tenants' skills as absent-upstream and could delete them. Filter existingSyncedSkills to rows whose tenantId matches the source's configured tenantId (absent = its own ambient bucket) before deleting, so a sync never removes another tenant's mirrored skills. Covered by a test where an ambient run leaves a tenant-b-owned skill untouched.

* fix: Apply tenant-scoped mirror deletion implementation

The prior commit (75ccfa3fc) added the test but the source change to github.ts was lost in an interrupted edit, leaving a failing test with no implementation. This adds the actual guard: the mirror-deletion pass skips skills whose tenantId does not match the source's configured tenantId (absent = ambient bucket), so an ambient source whose listSkillsBySource returns cross-tenant rows under non-strict isolation cannot delete another tenant's mirrored skills.

* fix: Resolve global access role outside tenant context for synced skill grants

Addresses Codex P2 (github.ts:1166): default access roles (incl. skill_viewer) are seeded globally with no tenantId under runAsSystem, but a tenant-scoped sync wraps ensurePublicViewer in the source's tenant context. The PermissionService grantPermission resolved the role via a tenant-isolated AccessRole query, so the global role did not match and tenant-scoped syncs failed with 'Role skill_viewer not found'. The sync adapter now resolves the role inside runAsSystem (matching the global seed) and writes the ACL entry in the active tenant context, so the AclEntry is tenant-scoped (visible to tenant users) while the role lookup still succeeds. Covered by service tests for the resolve-vs-write split and the missing-role failure.

* fix: Strip placeholder frontmatter booleans and check skill conflict before file sync

- 1083 (github.ts:759): toCleanFrontmatter now drops a non-boolean always-apply (e.g. the 'always-apply:' / 'always-apply: # TODO' placeholder, which js-yaml yields as null). The boolean is already captured in the dedicated alwaysApply field; persisting null left ambiguous frontmatter on the synced skill.
- 1080 (github.ts:1057): for an existing mirrored skill, check for an external content edit (via getSkillById + hasExternalSkillEdit) BEFORE syncSkillFiles mutates the bundled files, so a concurrently edited skill fails fast with SKILL_CONFLICT without partial file rewrites. The post-file-sync check still guards edits that land during the file sync window.
Tests: placeholder always-apply is dropped from synced frontmatter; concurrent-edit conflict leaves files unmutated (no upsert/delete).

* fix: Harden GitHub skill sync review paths

* fix: Reuse moved GitHub skill mirrors

* fix: Scope GitHub sync identity conflicts

* test: Fix GitHub sync conflict mock typing

* fix: Support nested env-backed skill sync

* fix: Keep skill sync config base-only

* fix: Scope GitHub skill identity lookup by tenant

* fix: Harden GitHub skill sync admin gates

* fix: Guard existing skill sync permission grants

* feat: Trigger skill sync from resolved config

* fix: Scope resolved skill sync by tenant

* test: Allow manual skill sync status tenant scoping

* refactor: Extract skill sync trigger orchestrator

* test: Complete orchestrator status fixture

* chore: Bump data provider version

* fix: Restrict skill sync server credentials

* test: Complete admin skill sync status fixtures

* fix: tighten skill sync trigger safeguards

* fix: preserve alwaysApply skill sync alias

* chore: sort skill sync imports

* fix: preserve skill sync request scope

* fix: harden skill sync review edges

* refactor: move skill sync admin access to api package

* fix: add skill sync declaration return types

* fix: satisfy skill sync type checks

* fix: resolve codex skill sync review findings

* fix: harden skill sync review edges

* fix: resolve codex skill sync edge findings

* fix: satisfy API declaration build after rebase
2026-06-10 21:05:54 -04:00
Danny Avila
470be2395f
feat: Surface Model Spec Branding on Landing and Selector (#13662)
Adds an opt-in showOnLanding flag to model specs. When set, the chat
landing shows the spec's label and description in place of the
time-of-day greeting; specs without the flag are unaffected, so existing
deployments see no behavior change. HTML-valued descriptions (inline
icons + markup) render sanitized via the shared config-HTML sanitizer
with a new media tag/attribute allowlist, both on the landing and in
model selector items. Excludes e2e specs from the typed client lint
block so staged e2e files no longer fail pre-commit with 'file not
found in project'.
2026-06-10 21:02:22 -04:00
Danny Avila
a52c82489e
🚷 fix: Reject Client-Supplied Subagent Configuration (#13660) 2026-06-10 16:10:59 -04:00
Dustin Healy
5867f1a065
🛡️ 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.
2026-06-10 09:03:05 -04:00
Danny Avila
f074bd9e09
📦 chore: Bump jest-junit to v17.0.0 2026-06-09 20:38:30 -04:00
Danny Avila
ca26a2dc9c
🛰️ 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
2026-06-09 20:12:31 -04:00
Danny Avila
a7f16911b2
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.
2026-06-09 17:50:02 -04:00
Danny Avila
2aea5f4a3a
📖 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.
2026-06-09 16:22:39 -04:00
Danny Avila
8fc2314208
🧠 fix: Bound Memory Agent Input (#13606) 2026-06-09 14:38:21 -04:00
Danny Avila
753e53eddd
🛬 fix: Coalesce Auth Recovery into a Single Refresh Flight (#13618)
* fix auth recovery singleflight

* add auth recovery e2e coverage

* handle invalid auth redirect timestamp
2026-06-09 12:04:12 -04:00
Danny Avila
2a956f143d
🪞 fix: Preserve Model Spec Icons Across Stream Resume and Abort (#13603)
Some checks are pending
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
2026-06-08 17:14:21 -04:00
Danny Avila
fb87abe773
🧩 feat: Enable Model Spec Subagents (#13598)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
Publish `@librechat/client` to NPM / pack (push) Has been cancelled
Publish `librechat-data-provider` to NPM / pack (push) Has been cancelled
Publish `@librechat/client` to NPM / publish-npm (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
2026-06-08 11:43:08 -04:00
Danny Avila
fbc51f6e66
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`.
2026-06-08 11:09:16 -04:00
Danny Avila
6edbafd09d
⬆️ 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.
2026-06-07 22:20:44 -04:00
Danny Avila
192703e041
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.
2026-06-07 21:40:48 -04:00
René Mulder
c80c54eefb
🔐 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.
2026-06-07 21:39:44 -04:00
Danny Avila
cb1d536874
📻 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
2026-06-07 10:45:54 -04:00
Danny Avila
265d660076
👷 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.
2026-06-06 18:40:31 -04:00
Danny Avila
83bdd3d65d
🌱 feat: Support Soft Default Model Spec (#13554)
* feat: add soft default model spec

* chore: sort ChatRoute imports
2026-06-06 14:20:59 -04:00
Danny Avila
2c8d54e18c
🗂️ feat: Add Deployment Skill Directory (#13523)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* feat: Add deployment skill directory

* chore: Address deployment skill review feedback

* fix: Include deployment skill file metadata

* test: Add deployment skills e2e smoke test
2026-06-05 10:24:28 -04:00