mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-07-01 03:43:03 +00:00
4555 commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bd158905b3 |
🔒 fix: Harden admin OAuth refresh against user bans, tenant scope gaps, and cross-tenant migration
Post-identity-resolution ban check: the initial checkBan middleware fires before the
refresh token is exchanged and req.user is populated, so it can only evaluate IP bans.
After applyGoogleAdminRefresh/applyAdminRefresh resolves the user identity, we now
synthesize req.user and re-run checkBan against the resolved user's id before emitting
the JWT, so a user-level ban is enforced even from a fresh IP.
Domain allowlist now includes userId: the getAppConfig call in isEmailAllowedForUser
was passing only role, missing user and group-level allowedDomains overrides that the
initial OAuth callback's checkDomainAllowed enforces via userId. Both branches now
pass userId so buildPrincipals takes the full user+group+role resolution path. The
tenant branch is also inlined (replacing resolveAppConfigForUser) to accept userId,
wrapped in tenantStorage.run for correct Mongoose scoping and cache-key resolution.
Cross-tenant email-fallback migration: the Passport verify callback fires before
tenantContextMiddleware, so findUser({email}) is unscoped and can return a same-email
user from another tenant. Writing googleId onto that document permanently corrupts
the other tenant's account. Migration is now blocked for users with a tenantId;
single-tenant users are unaffected.
|
||
|
|
0f14dcce62 |
🔒 fix: Add ban check and fix domain allowlist on admin OAuth refresh
Two gaps in the /api/admin/oauth/refresh route:
Add middleware.checkBan to the route chain before preAuthTenantMiddleware,
matching the gate that /login/local and createOAuthHandler already apply.
Without it a banned admin could keep minting JWTs until their IdP refresh
token expired.
Replace getAppConfig({ baseOnly: true }) in the non-tenant isEmailAllowed
closure with getAppConfig({ role: user.role }), which includes DB-layer
overrides from the admin panel. baseOnly returns only YAML-derived config,
so any allowedDomains list maintained entirely through the admin panel was
silently inert on this path. Extract isEmailAllowedForUser as a shared
helper, move it into buildAdminRefreshClosures so both Google and OpenID
refresh paths enforce domain policy consistently, and add isEmailAllowed
to AdminRefreshDeps in the TS package so applyAdminRefresh can invoke it.
|
||
|
|
4b4cecb48f |
🔒 fix: Reject refresh for users migrated off the Google provider
The interactive Google admin login path in socialLogin.js already rejects a user whose provider field is not 'google', returning AUTH_FAILED. Without a matching guard in the refresh path, a user migrated to OpenID could use an unexpired Google refresh token to keep minting admin JWTs indefinitely. Add a PROVIDER_MISMATCH check after resolving the user in both the direct getUserById branch and the findUsers fallback branch of resolveAdminUser, mirroring the provider gate the interactive path enforces. |
||
|
|
fcdb66bb6b |
🔒 fix: Apply brutal-review hardening to Google admin refresh
Tighten the Google OAuth refresh flow against all outstanding code review findings: enforce JWT aud claim verification against the configured clientId (ISSUER_MISMATCH on mismatch), reject ambiguous googleId matches (limit:2 in findUsers, USER_ID_MISMATCH when multiple rows match), scope the authInfo refresh-token carrier to the Google provider only, add TOCTOU re-read defense after the admin googleId migration write in socialLogin, deduplicate canAccessAdmin/mintToken closures via buildAdminRefreshClosures shared by both OpenID and Google refresh paths, document rotation semantics on AdminExchangeResponse.refreshToken, standardise all log prefixes to [admin/oauth/refresh], and expand test coverage for all new paths. |
||
|
|
0e55d8a1df |
🔁 fix: Tighten Google admin refresh and limit social-login changes
Brutal-review findings on top of the upstream feature work.
socialLogin.js: the migrate-or-reject pattern from the previous commit
applied to every provider's chat-side verify callback, not just the admin
flow. Gate both branches on `options.existingUsersOnly` so the chat-side
googleLogin / facebookLogin / etc. keep their pre-existing email-fallback
behavior unchanged. Tests follow: restore the original `should fallback to
finding user by email` chat-side case and re-add the migration and
mismatch-reject cases as admin-only by passing `{ existingUsersOnly: true }`
to socialLogin in those tests.
googleRefresh.ts: add a defense-in-depth `isEmailAllowed(user)` dep that
the helper invokes before `canAccessAdmin`. Mirrors the
`isEmailDomainAllowed` check the initial Google admin login already runs,
so a deployment that removes a domain from `registration.allowedDomains`
after issuance can no longer mint fresh JWTs for that admin via refresh.
The route handler wires it up with `resolveAppConfigForUser` +
`isEmailDomainAllowed`, falling back to `baseOnly` config for users
without a tenantId.
googleRefresh.ts: drop the unreachable `?? ''` defensive coalescing in
`fetchGoogleTokenset`. The `GOOGLE_NOT_CONFIGURED` guard upstream already
narrows `clientId`/`clientSecret` to non-empty strings; the function
takes a narrowed `GoogleAdminRefreshConfiguredOptions` shape and
`applyGoogleAdminRefresh` constructs that shape after the guard.
|
||
|
|
21922eea78 |
🧹 refactor: Move Google admin refresh into TypeScript @librechat/api helper
Per repo guidance (CLAUDE.md): all new backend code must be TypeScript in /packages/api, and /api is a thin JS wrapper. The previous commit landed the Google admin refresh flow as ~120 lines of new JS inside api/server/routes/admin/auth.js, which violates that. This commit extracts the flow into a new TS helper at packages/api/src/auth/googleRefresh.ts and reduces the route handler to a thin dep-wiring wrapper. The helper exports applyGoogleAdminRefresh(deps, options) with the same shape as the OpenID applyAdminRefresh: callers pass findUsers, getUserById, canAccessAdmin, and mintToken as deps so the package stays free of /api model imports and capability/session helpers. The route handler now builds those deps from the existing model + capability + token modules and calls the helper, mapping AdminRefreshError to the documented HTTP responses. While moving the code, the helper now guards getUserById with Types.ObjectId.isValid before the direct-lookup branch, matching the OpenID admin path at packages/api/src/auth/refresh.ts. Without this guard a malformed user_id from the admin client would hit Mongoose findById's CastError and surface as a 500 INTERNAL_ERROR instead of falling through to the documented sub-based lookup. Tests move with the code: packages/api/src/auth/googleRefresh.spec.ts now owns the helper's behavior (token endpoint, userinfo fallback, ObjectId guard, USER_ID_MISMATCH/TENANT_MISMATCH/USER_NOT_FOUND/FORBIDDEN, rotated refresh-token pass-through, GOOGLE_NOT_CONFIGURED, IDP_INCOMPLETE on non-JSON body, CLAIMS_INCOMPLETE when both id_token and userinfo miss). The route-level api/server/routes/admin/auth.refresh.test.js drops the duplicated end-to-end Google cases and keeps a smaller surface: route delegates to applyGoogleAdminRefresh with the right deps + options, maps AdminRefreshError to HTTP status/code, falls through to 500 for unknown errors, and rejects unknown providers with INVALID_PROVIDER. |
||
|
|
984f81e257 |
🧪 fix: Add updateUser to appleStrategy test mock for socialLogin migration
The shared socialLogin verify callback now invokes `updateUser` when the email-fallback path discovers a same-provider user with an empty provider id, persisting the refreshed sub. The Apple strategy test's `~/models` mock did not stub `updateUser`, so the migration path hit `TypeError: updateUser is not a function` and failed the `should handle existing user and update avatarUrl` case in CI shard 1/3. |
||
|
|
1dddf97c4a |
🔁 fix: Harden Google admin refresh against bot review findings
Five validated findings from the initial bot pass: socialLogin.js: mirror the OpenID migrate-or-reject pattern on the email fallback. When an existing user is found by email and the stored provider id is empty, persist the refreshed sub so the refresh path can later bind to it. When the stored id is present and differs, reject as AUTH_FAILED to prevent identity-swap, matching the existing OpenID behavior in packages/api/src/auth/openid.ts. oauth.js: scope the non-OpenID admin refresh-token forwarding to provider === 'google'. The previous else branch would have forwarded a Discord refresh token (passport-discord supplies one) into the admin exchange payload even though /api/admin/oauth/refresh only accepts openid or google, leaving the admin client with a token it could not refresh. admin/auth.js (refreshGoogleAdminSession): drop id_token from the mandatory-fields check. Google's OAuth refresh response is documented to include id_token only conditionally, so the previous mandatory check broke refresh whenever Google omitted it. Decode id_token when present (fast path); when absent, call Google's userinfo endpoint with the access token to read sub. Wrap tokenResponse.json() in try/catch and return IDP_INCOMPLETE on parse failure instead of a generic 500. Tighten access_token to a typeof string check. admin/auth.js (refreshGoogleAdminSession): reuse serializeUserForExchange for the response user so the Google refresh shape matches /oauth/exchange and the OpenID branch exactly (full _id, id, email, name, username, role, avatar, provider, openidId). The previous Google-specific subset dropped fields the admin client relies on for later provider-specific refreshes and disambiguation. Tests cover each fix: socialLogin's migration and rejection cases, the oauth.js Discord-gating case, the userinfo fallback path on missing id_token, CLAIMS_INCOMPLETE when both id_token and userinfo are absent, IDP_INCOMPLETE on a non-JSON token body, and the full response shape on the happy path. |
||
|
|
d40c51616e |
🔑 feat: Refresh-Capable Google Admin OAuth Sessions
Google admin sessions cannot be refreshed today. Three gaps add up to that:
passport.authenticate('googleAdmin', ...) in api/server/routes/admin/auth.js
never sets access_type=offline, so Google omits the refresh_token from its
token response; createOAuthHandler in api/server/controllers/auth/oauth.js
only forwards a refresh token into the admin exchange payload when the user's
provider is 'openid' AND OPENID_REUSE_TOKENS is enabled; and
/api/admin/oauth/refresh is openid-only, calling openid-client.refreshTokenGrant
against the configured OIDC issuer. OpenID admins refresh transparently
because all three are in place for them.
This PR closes all three. The googleAdmin authenticate call now passes
accessType: 'offline' and prompt: 'consent' so Google issues a refresh token
on consent; the chat-side googleLogin is untouched. The shared socialLogin
verify callback now passes the IdP refreshToken through as passport's third
argument (info), landing on req.authInfo, with the two-argument call shape
preserved when no refresh token is present so existing strategy tests stay
valid. createOAuthHandler reads req.authInfo?.refreshToken for non-OpenID
admin providers and forwards it into the exchange code; the OpenID branch
and its OPENID_REUSE_TOKENS gate are unchanged. /api/admin/oauth/refresh
now accepts an optional provider field ('openid' | 'google', default 'openid').
The new Google branch POSTs grant_type=refresh_token to
https://oauth2.googleapis.com/token, decodes the returned id_token for the sub
claim, looks up the admin user by googleId, enforces tenant scope and
ACCESS_ADMIN, and mints a fresh LibreChat JWT in the same response shape
/oauth/exchange returns. It is gated on GOOGLE_CLIENT_ID and
GOOGLE_CLIENT_SECRET being set (returns 503 GOOGLE_NOT_CONFIGURED otherwise);
unknown provider values return 400 INVALID_PROVIDER.
|
||
|
|
9de3249e9c
|
🎛️ feat: Redesign Settings with Registry-Driven Dialog, Search, and Mobile Drill-In (#13722)
* i18n: add settings reorganization keys
* feat(settings): add tab/section types and tab metadata
* feat(settings): add useSettingsContext guard hook
* feat(settings): add pure settings search filter with tests
* feat(settings): extract selectors and add control wrappers
* feat(settings): add setting registry, memory and billing controls, integrity test
* feat(settings): add Section and Advanced disclosure with test
* feat(settings): add content pane with tab and search views
* feat(settings): add sidebar and dialog shell with tests
* refactor(settings): wire new dialog and remove superseded containers
* fix(settings): restore speech external engine option, escape-to-clear search, results a11y
- SpeechControls.tsx: read sttExternal/ttsExternal from useGetCustomConfigSpeechQuery
instead of hardcoding false, so external engine options appear on qualifying deployments
- Sidebar: Escape clears search input when non-empty, stops propagation to avoid closing dialog
- Content: persistent aria-live="polite" wrapper covers both populated results and empty state
- context: useMemo on returned ctx object so Content's useMemo deps are referentially stable
- locales/README.md: update stale path from deleted General.tsx to Selectors.tsx
* refactor(settings): reorganize categories, remove advanced disclosure, add About
- Re-categorize settings into logical groups (username display -> Chat/Messages,
keep-screen-awake -> Accessibility, fork/prompts surfaced into Chat sections)
- Dissolve thin Personalization tab; move Memory into Data & Privacy
- Remove the Advanced collapsible; all settings always visible, destructive
actions grouped in an always-visible Danger zone
- Wire the new About tab into the registry-driven dialog
- Standardize spacing with bordered, evenly-divided section cards
- Use semantic text-text-* / border tokens so dark mode renders correctly
- Sync LangSelector language-loading indicator from dev
* feat(settings): move archived chats to the account menu
Add an Archived chats item to the account dropdown next to My Files,
opening the archived chats table in a modal. Removes it from the
settings dialog where it no longer fit the data/privacy grouping.
* feat(settings): polish About panel and use shared CopyButton
- Flatten the build-info into a single divided key/value list (drop the
redundant inner card now that it sits inside a section card)
- Replace the hand-rolled copy button with the shared animated CopyButton
- Shorten the copied label so it fits the button without clipping
* fix(settings): set primary text color on setting rows for dark mode
Leaf control labels rendered without a text color and fell back to the
browser default (black), making them invisible on the dark panel. Set
text-text-primary on the section and search-results row containers so
labels inherit a visible color, matching the old container behavior.
* fix(settings): use visible icon for dialog close button
The plain multiplication-sign close button had no text color and was
invisible on the dark panel. Replace it with the lucide X icon using
text-text-secondary/hover:text-text-primary so it shows in both themes.
* fix(nav): drop focus ring on account menu items, use hover background only
The account-settings popover drew a 2px ring around the active menu item.
Remove that override so items show only the standard hover background,
consistent with every other menu.
* fix(settings): replace native search clear with a real X button
The settings search used type=search, whose native WebKit clear control
rendered as a blue X. Switch to a text input and add a real lucide X
clear button styled text-text-secondary, shown only when there's a query.
* fix(speech): disable dependent dropdowns and switches when STT/TTS is off
Add a disabled prop to the shared Dropdown component, then gate the
speech engine/voice/language dropdowns and the automatic-playback switch
on their parent toggle (speechToText / textToSpeech), matching the
controls that already disabled correctly.
* feat(settings): mobile drill-in navigation for settings tabs
On small screens the horizontal scrolling tab row is replaced with a
full-width vertical list (with chevrons); tapping a tab drills into its
content with a Back header. Searching shows results full-width. Desktop
keeps the side-by-side sidebar + content layout unchanged.
* chore(settings): remove orphaned i18n keys, fix import order and review notes
- Drop the i18n keys left unused after the refactor (old Commands/Balance/
Personalization tab labels, the Speech simple/advanced labels, and the
former About section headings)
- Sort imports in the rebased files the lint-staged hook never touched
- Guard the language fallback against an empty navigator.languages
- Import the RefObject type instead of leaning on the React namespace
* feat(settings): searchable language dropdown
Add an opt-in searchable mode to the shared Dropdown (Ariakit Select +
Combobox) and use it for the language selector, which has 40+ options.
The trigger styling is unchanged so it stays consistent with the other
settings rows; only the popover gains a filter input.
Accessibility: the filtered listbox is labeled, the empty state is moved
out of the listbox and announced via an aria-live status region, and the
decorative selected-state checkmark is hidden from assistive tech.
* fix(settings): restore guards dropped in dialog refactor
- Fall back to the General tab when the active tab becomes hidden
(e.g. About when buildInfo is disabled) instead of rendering an
empty panel.
- Normalize a deprecated/invalid engineTTS (e.g. 'edge') back to
browser during speech init so read-aloud controls keep rendering.
- Hide the cloud browser voices toggle unless Browser TTS is active.
* test(e2e): match agent-creation toast exactly to avoid SR-announce collision
The agent builder spec asserted the creation toast with a non-exact
getByText, which also matched Radix Toast's transient role="status"
announce region ("Notification Successfully created ..."), causing a
strict-mode violation. Mirror the mcp spec by using { exact: true }.
* fix(settings): render the active panel as a tabpanel
Wrap the non-search settings body in Tabs.Content so the selected
panel gets role=tabpanel with Radix's id/aria-labelledby wiring,
resolving the aria-controls target on each tab trigger. Search
results stay a labeled live region (the tab list is hidden during
mobile search, so a tabpanel aria-labelledby would dangle).
|
||
|
|
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.
|
||
|
|
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 |
||
|
|
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 |
||
|
|
27b0782201
|
📛 feat: Tag Langfuse Traces With Tenant ID (#13808)
* feat: tag Langfuse traces with tenant id * fix: propagate tenant id to agent Langfuse config |
||
|
|
8628897c9c
|
📦 chore: Bump @librechat/agents to v3.2.37 (#13826)
|
||
|
|
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 |
||
|
|
49f4b659f6
|
🔐 fix: Honor Admin-Panel MCP Allowlist Overrides Without Restart (#13814)
* 🔐 fix: Honor Admin-Panel MCP Allowlist Overrides Without Restart MCPServersRegistry was built once at boot from getAppConfig({ baseOnly: true }), freezing allowedDomains/allowedAddresses to YAML. Admin-panel mcpSettings overrides were ignored by both inspection (addServer/ reinspectServer/updateServer/lazyInitConfigServer) and runtime connection enforcement (assertResolvedRuntimeConfigAllowed), so a domain allowed only via the panel failed inspection and never connected. Make the registry's effective allowlists mutable and refresh them from the merged admin-panel config: seed at boot, and re-apply on every config mutation via invalidateConfigCaches -> clearMcpConfigCache. Both inspection and connection paths read the same getters, so both honor overrides without a restart. Fail-safe: current allowlists are preserved when the merged read fails. * 🛡️ fix: Scope MCP allowlist refresh to global config, fail-safe on DB error Address Codex P1 review findings on the allowlist-refresh path: - Tenant-scoped config mutations no longer push one tenant's merged mcpSettings into the process-wide registry singleton (read by all MCP connection paths), which would leak allowlists across tenants. Only global (non-tenant) mutations refresh the registry; tenant mutations still evict the config-server cache. - The refresh read now uses strictOverrides:true so a transient DB error throws instead of silently returning YAML base config — preserving the last-known allowlists rather than overwriting them with fallback values. Adds the strictOverrides option to getAppConfig (default off, no behavior change for existing callers). * ♻️ refactor: Resolve MCP allowlists per-request (tenant-scoped) instead of a global singleton Supersedes the prior global-mutation approach. MCP allowlists live in mcpSettings, which is tenant/principal-scoped admin config, so a process-wide singleton value is the wrong model — it caused cross-tenant bleed and stale reads. Instead, inject a resolver (from the app layer, where the merged config lives) that the registry calls per inspection and per connection. It reads the ALS tenant context via getAppConfig and accepts the acting user so user/role-scoped overrides resolve; config-source inspection (no user) resolves at tenant scope. Falls back to the YAML base allowlists when no resolver is set or the lookup fails, so a transient error fails to the operator baseline rather than disabling the allowlist. Removes the now-unnecessary setAllowlists / boot-seed / invalidateConfigCaches refresh / getAppConfig.strictOverrides machinery. * 🔒 fix: Scope config-source cache by allowlist; resolve OAuth allowlists per-request Address Codex review of the per-request resolver: - Config-source cache key now folds in the resolved allowlists, not just the raw-config hash. Inspection results became allowlist-dependent, so without this a tenant whose allowlist rejects a URL could poison the shared key with an inspectionFailed stub for a tenant that allows it (and vice versa). The tenant-scoped allowlist is resolved once per ensureConfigServers pass and threaded through the cache key + inspection. - The two remaining request-time OAuth allowlist reads now use the merged config instead of the YAML base getters: the fallback OAuth-initiate path (routes/mcp.js) via resolveAllowlists, and OAuth revocation (UserController.maybeUninstallOAuthMCP) via the request's already-merged appConfig.mcpSettings. Without this, an OAuth endpoint allowed only by an admin-panel override was rejected while inspection/connection allowed it. * ✅ test: Update MCP OAuth registry/config mocks for per-request allowlists CI fix for the Finding-12 change. The OAuth-initiate route now calls registry.resolveAllowlists() and the revocation path reads the merged appConfig.mcpSettings, so the affected specs' mocks were asserting the old base-getter values: - routes/__tests__/mcp.spec.js: add resolveAllowlists to the registry mock. - UserController.mcpOAuth.spec.js: provide mcpSettings on the getAppConfig mock so revokeOAuthToken still receives the expected allowlists. * 🧪 test: e2e proof that admin-panel MCP allowlist override takes effect Adds a Playwright mock-harness spec for #13809. A URL-based MCP fixture (e2e-http, streamable-http SDK server) boots inspectionFailed because its origin is omitted from the YAML mcpSettings.allowedDomains; the spec adds that origin via an admin config override (PUT /api/admin/config/user/:id) and asserts the server reinitializes — exercising the real resolver path through the backend + DB. Before the fix, reinspection used the frozen YAML allowlist and the server stayed unreachable. - e2e/setup/fake-mcp-http-server.js: streamable-HTTP MCP fixture (health GET /). - e2e/playwright.config.mock.ts: boot the fixture as a second webServer. - e2e/config/librechat.e2e.yaml: mcpSettings.allowedDomains (excludes 127.0.0.1) + the e2e-http server. - e2e/specs/mock/mcp-allowlist-override.spec.ts: login → baseline reinit fails → apply override → reinit succeeds. |
||
|
|
c04bddd304
|
🪵 refactor: Bound Log Traversal And Remove Legacy api/config Logger (#13813)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🛡️ fix: Bound object-traverse against DAG fan-out and shared refs Detect cycles via the ancestor chain (so shared, non-circular references in sibling branches / DAGs are traversed correctly) and add defensive maxNodes (100k) / maxDepth (100) caps. The removed global visited set was implicitly bounding work at O(distinct nodes); ancestor-chain-only detection is O(root-to-node paths), exponential on DAGs (a depth-24 diamond went from 26 to 50M visits / 1.6s of synchronous work). The caps bound it to ~9ms while leaving normal traversal untouched. Adds a spec covering shared refs, cycles, DAGs, and both bounds. The lone consumer, debugTraverse, inherits the defaults with no change. * 🪵 refactor: Remove legacy api/config logger duplicate The api/config winston logger was a stale parallel implementation of the canonical @librechat/data-schemas logger, with unbounded redaction (regex-only redactFormat, npm traverse-based debugTraverse). Its winston instance and the logger export from api/config/index.js had zero consumers — every ~/config importer uses the MCP/flow-manager exports. The only live tie was ToolService's use of redactMessage. Re-export redactMessage from @librechat/data-schemas (behaviorally identical, a superset of the regex set), point ToolService at it, delete api/config/winston.js and api/config/parsers.js, drop the dead logger export, and remove the orphaned ~/config/parsers mock from the global test setup. * 🧹 chore: Drop orphaned traverse dep and stale legacy logger tests Deleting api/config/{winston,parsers}.js left the npm 'traverse' package unused in api/package.json (flagged by the detect-unused-packages CI check) and orphaned two tests that imported the deleted modules. Remove the traverse dependency (sync package-lock), and delete api/config/__tests__/{parsers,logToFile}.spec.js — the canonical logger's behavior is covered by packages/data-schemas/src/config/parsers.spec.ts. * 🩹 fix: Make object-traverse caps bound work and survive update() Address Codex review: (1) break the child loops as soon as the node budget is spent and iterate objects via for...in instead of materializing Object.entries/Object.keys, so maxNodes actually bounds work for wide arrays/objects; (2) detect ancestor cycles against an immutable original-node stack rather than context.node, which a callback's update() can reassign (the debug formatter rewrites array nodes in place). Adds tests for the wide-array bound and the update()-cycle case. * 🎚️ fix: Tighten object-traverse defaults to a ~1ms log budget Lower maxNodes 100000 -> 2500 and maxDepth 100 -> 5. Measured cost is ~140ns/node with the debug formatter callback, so 2500 nodes keeps a single log under ~1ms even on slower prod hardware; real log objects are ~25-30 nodes at depth 3-4, leaving ample headroom. maxNodes is the fan-out/cost lever; maxDepth bounds recursion and output readability (depth-5 covers typical logs, deeper renders compactly). |
||
|
|
6055ad0af2
|
🪃 fix: Restore Raw Spec Fallback for Enforced Presets (#13804)
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
* fix: rebuild enforced specs from preset * test: Add enforced model spec e2e coverage * test: Align enforced spec regression scope |
||
|
|
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
|
||
|
|
c820dfb9a0
|
🛤️ ci: Limit GitNexus Deploys To Main And Dev Only (#13799) | ||
|
|
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 |
||
|
|
054fa4bfa7
|
🥽 fix: Restrict MCP Server URL Disclosure to Admins, Owners, and Editors (#13784)
* 🥽 fix: Redact Non-User-Sourced MCP Server URLs by ACL Edit Permission GET /api/mcp/servers and GET /api/mcp/servers/:serverName return MCP server configs to any caller with MCP-use permission. For user-sourced configs (DB-stored, UI-submitted), the URL is the caller's own and is intentionally disclosed. For non-user-sourced configs (YAML or config-tier, operator-defined), the URL and OAuth flow endpoints (authorization_url, token_url) are operator-sensitive: they can encode internal infrastructure hostnames and are not editable through the API. This change redacts those fields on non-user-sourced configs unless the caller has edit authority on the resource, using the same ACL check (PermissionBits.EDIT) that the PATCH and DELETE routes already enforce via canAccessMCPServerResource. Callers with broad MANAGE_MCP_SERVERS capability bypass the per-resource check, matching the existing capability bypass in canAccessResource. customUserVars is intentionally not redacted: its values are UI hint metadata (title, description, sensitive), not user-supplied secrets; blanking it would give non-editor callers a Configure form with no field labels. * 🥽 fix: Correct getResourcePermissionsMap import path + tighten redact comments The MCP server redaction commit imported getResourcePermissionsMap from ~/server/controllers/PermissionsController, but that controller is a consumer of the helper, not its exporter. The canonical export lives in ~/server/services/PermissionService (which controllers/agents/v1.js already imports from). Fixes the runtime getResourcePermissionsMap is not a function failure on GET /api/mcp/servers and the four downstream route-spec failures whose config mocks lacked a source field and were therefore wrongly treated as non-user-sourced; mocks now reflect the real registry behavior (addServer/updateServer tag DB-stored configs with source: 'user'). Trims narrating JSDoc on the redact helpers and resorts the librechat-data-provider destructure by length. * chore: import order * 🥽 fix: Redact OAuth Revocation Endpoint Alongside Authorization And Token URLs The OAuth-URL strip path only dropped authorization_url and token_url. The UserOAuthOptionsSchema in packages/data-provider/src/mcp.ts (line 146) accepts revocation_endpoint as another operator-configurable URL, and the OAuth handler uses it to revoke tokens; it can hold the same internal IdP hostnames the existing strip is trying to hide. Adds revocation_endpoint to the destructure so a non-user-sourced YAML/config MCP server config no longer leaks the revocation URL to non-editor callers. The existing strip url and oauth flow URLs spec is extended with a revocation_endpoint value to lock in the new field. * 🥽 fix: Gate Shared DB Server URL Disclosure On ACL Edit Permission source-driven URL disclosure was incorrect for shared DB-backed MCP servers. ServerConfigsDB.mapDBServerToParsedConfig (packages/api/src/mcp/registry/db/ServerConfigsDB.ts:465) sets source: 'user' on every DB-stored config it returns, regardless of who is accessing it. A user with only VIEW share on a DB server, or with agent-mediated access, was therefore treated by the redaction layer as if they owned the URL, and GET /api/mcp/servers disclosed the owner's URL and OAuth flow URLs to viewers who could not edit the resource. The redaction is now driven purely by ACL edit authority: computeCanEditByServer routes every dbId-bearing config through PermissionBits.EDIT regardless of source; redactServerSecrets strips on !canEdit regardless of source. POST and PATCH controllers explicitly pass canEdit: true since both endpoints establish edit authority (POST creates the resource, PATCH is gated on the EDIT middleware). Legacy/ephemeral configs without a dbId still fall back to the source heuristic. * 📝 docs: correct redactServerSecrets URL-disclosure comment --------- Co-authored-by: Danny Avila <danny@librechat.ai> |
||
|
|
d0f659fa75
|
🗜️ fix: Support Windows ZIP MIME Uploads (#13794) | ||
|
|
b91c1c2508
|
🎤 fix: Keep Microphone Icon Visible On Initial Chat Render (#13788)
* 🎤 fix: Keep Microphone Icon Visible On Initial Chat Render AudioRecorder returned null while the parent ChatForm's textAreaRef was still null on first paint, hiding the mic icon until an unrelated re-render. Render the button disabled instead so the icon is always present. Closes #13786 * 🎤 refactor: Drop Unused textAreaRef Dependency From AudioRecorder Per Codex review: deriving the button's disabled state from textAreaRef.current could leave the mic permanently disabled until an unrelated re-render, since assigning a ref does not trigger one. The handlers never read the ref, so remove the dependency entirely along with the now-unused prop. |
||
|
|
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 |
||
|
|
055585f9f1
|
🪢 fix: Tie MCP Cleanup To Resumable Runs (#13769)
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
Publish `@librechat/client` to NPM / pack (push) Has been cancelled
Publish `librechat-data-provider` to NPM / pack (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / pack (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Helm Chart Tags / Ignore non-main push (push) Has been cancelled
Sync Helm Chart Tags / Sync chart tags (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
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
* fix: Clean up request-scoped MCP connections * test: Format MCP request context spec * refactor: Move MCP request context to API package |
||
|
|
0537930144
|
🗂️ fix: Scope Token Config Cache (#13770)
* fix token config tenant cache scope * fix token config scoped cache backfill * chore sort token config imports |
||
|
|
62ed62ac6e
|
🏘️ fix: Scope Skill Sync Status (#13771)
* fix: Scope Skill Sync Status by Tenant * fix: Preserve Unscoped Skill Sync Status * fix: Filter Inherited Skill Sync Sources |
||
|
|
ec94437854
|
🧾 refactor: Disable Context Cost By Default (#13768) | ||
|
|
bf946975ca
|
🫷 fix: Withhold Anthropic Custom Headers From User URLs (#13767) | ||
|
|
23c9226e9c
|
🌍 i18n: Update translation.json with latest translations (#13766) | ||
|
|
5986a1c6d2
|
📊 chore: Bump Helm chart version to 2.0.6 | ||
|
|
b917e0418b
|
✨ v0.8.7-rc1 (#13592)
* chore: Bump LibreChat to v0.8.7-rc1 * docs: Sync Chinese README |
||
|
|
fbc990f684
|
📈 fix: Isolate RUM Telemetry Proxy Auth from App Auth (#13765)
* fix(rum): isolate telemetry proxy auth * feat(rum): track proxy error metrics * refactor(rum): simplify proxy auth strategy flow * test(rum): clarify proxy success metric assertion * test(metrics): use typed supertest import * test(metrics): add local supertest types * test(metrics): keep supertest types local * test(metrics): use official supertest types * fix(rum): log proxy auth strategy errors * fix(rum): classify proxy auth errors in metrics * style(rum): sort telemetry metric imports * ci: mention import sort check command * ci: show targeted import sort example |
||
|
|
bc5a3f502f
|
📡 refactor: Gate Noisy Redis OTEL Instrumentation (#13764)
* fix(telemetry): gate noisy database instrumentation * test(telemetry): assert opt-in instrumentation baseline * style(telemetry): sort imports * fix(telemetry): preserve mongoose internal tracing by default * fix(telemetry): limit database tracing flag to redis |
||
|
|
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. |
||
|
|
44c253d48a
|
🪙 fix: Correct Context Usage Gauge After Summarization (#13744)
* 🪙 fix: Persist Context Snapshot + Summary Marker After Summarization The post-summarization context is correctly compacted by the SDK, but the breakdown wasn't reliably reaching the client, leaving the gauge on the whole-history estimate (stuck at 100% forever once a conversation compacts). Two server changes in buildResponseMetadata: - Snapshot guard: persist the breakdown when a PRIMARY usage event follows the latest snapshot (tracked via contextUsageSink.latestUsageIndex, recorded in the on_context_usage handler) instead of a brittle snapshot-vs-primary count. A summarization detour adds an extra snapshot whose only following usage is tagged 'summarization', which the count guard could miscount and drop. - Summary marker: whenever a turn compacts (summaryTokens > 0), persist a lightweight metadata.summaryUsedTokens (the pre-invoke compacted context size) UNCONDITIONALLY — so even when the full snapshot can't be saved (interrupted final call) or never reaches the client, the per-message estimate has a signal to cap the discarded history. Tests: client.contextMetadata.spec (guard + marker, incl. marker-survives-drop) and a real-pipeline summarization integration test. * 🪙 fix: Cap the Context Estimate at the Summary Marker When the gauge falls back to the per-message estimate (no usable snapshot on the branch), sumBranch summed the ENTIRE branch history — after a summarization that discarded most of it, this over-counts and pins the gauge at 100% in perpetuity. sumBranch now stops at the deepest summarized response (metadata.summaryUsedTokens) and records it as summaryBaseline; the walk counts only post-summary messages, and useTokenUsage adds the baseline. So the estimate reflects the compacted context (summary + recent turns), not the discarded history. USD/default behavior unchanged when no marker is present. Test: sumBranch caps a huge pre-summary history at the compacted baseline. * 🪙 fix: Address Codex Review on the Summarization Marker - Branch cost/usage is no longer truncated at the summary marker — sumBranch caps only the CONTEXT-window count there and keeps accumulating provider usage/cost to the root (cumulative spend isn't discarded by compaction). - findBranchSnapshotAnchor stops at a summarized response with no snapshot of its own, so it can't recover a stale PRE-summary snapshot and show discarded history; the summary-baseline estimate is used instead. - Abort path: buildAbortedResponseMetadata now persists the summaryUsedTokens marker (pre-invoke, no completedOutputTokens ambiguity, so safe on abort) so a STOPPED summarized turn isn't re-summed on reload. - Marker baseline fallback now includes summaryTokens (a separate breakdown field) so it doesn't under-report the compacted size. DRY'd into a shared computeSummaryUsedTokens used by the completion and abort paths. - Estimate popover surfaces the summary baseline as a row so the displayed rows reconcile with the header total. Tests: sumBranch cost-not-truncated + anchor-stops-at-marker (client); computeSummaryUsedTokens fallback + abort marker (packages/api). * 🪙 fix: Attribute Persisted Context Usage to the Snapshot Run Match the post-snapshot primary usage to the latest snapshot's runId before persisting metadata.contextUsage. Parallel/direct runs interleave snapshots and usage (A snapshot → B snapshot → A usage → B no-usage); the prior index-only guard persisted B's snapshot with A's output. finalCallOutputTokens now filters completedOutputTokens to the snapshot's run. Untagged events (older lib/resume) match any run for back-compat. * 🪙 fix: Harden Summary Marker Against Tool-Loops, Stale Anchors, and Emit Races Codex round on the summarization marker: - Avoid double-counting earlier tool-loop outputs in the summary marker: those outputs sit in BOTH the latest snapshot's pre-invoke baseline AND the response message's tokenCount the client estimate adds on top. computeSummaryUsedTokens now subtracts the run's prior primary outputs (priorRunOutputTokens) — the live path bounds them by the snapshot's usage index, the abort path by all primaries (an interrupted final call emits none). Single-call turns subtract 0. - Stop treating pre-summary anchors as active: sumBranch no longer sets containsAnchor once the context is capped at a summary marker, so a stale pre-summary snapshot can't override the summary-baseline estimate. - Capture latestUsageIndex BEFORE awaiting emitEvent: a yield (resumable SSE / Redis) during parallel runs could let this call's own usage advance the index past the event that proves the snapshot completed, dropping a valid breakdown. * 🪙 fix: Subtract Summarization Output from the Summary Marker recordCollectedUsage folds the summarization call's completion into the response message's tokenCount, while the generated summary is also in the snapshot baseline as summaryTokens. The client estimate (summaryBaseline + responseTokenCount) thus counted the summary twice — inflating the gauge after compaction even on a single-call turn whenever the full snapshot is unavailable. priorRunOutputTokens now also counts summarization-tagged output (still excluding subagent/sequential, which recordCollectedUsage keeps out of the reported total), so the marker subtracts it. Updated unit + guard tests. * 🪙 fix: Refine Marker Subtraction for Summarization RunId and Abort Boundary Two Codex follow-ups on the marker-subtraction logic: - Subtract summarization output regardless of runId: the summarize detour is its own model-end call that may carry a distinct runId, but its output still lands in this response's tokenCount AND the snapshot baseline (summaryTokens). It is now counted unconditionally (still within the response's own usageEmitSink), while primaries keep the parallel-run runId filter. - Don't subtract primaries on the abort path: the job stores no snapshot/usage boundary, so a primary that completed AFTER the latest snapshot is NOT in the baseline; subtracting it would cancel real output and under-report. priorRun- OutputTokens gains an includePrimary flag (false for abort) — abort subtracts only the always-pre-snapshot summarization output. * 🪙 fix: Run-Scope Summary Subtraction and Stop Subtracting on Abort Two Codex follow-ups, resolved by reverting the round-4 detour: - Run-scope the summarization subtraction: the summarize detour inherits the graph run id (traceConfig spreads config.metadata.run_id), so its usage shares the answer snapshot's runId — it is NOT a distinct run. priorRunOutputTokens now filters summarization by runId like primaries, so a parallel sibling run's summary (different runId, in the sibling's baseline) is no longer subtracted from this branch's marker. Drops the includePrimary flag added last round. - Stop subtracting on the abort path: abort tokenCount is countTokens(text) (abortMiddleware) or absent (agents route) — it does not fold in summarization or earlier-call output the way recordCollectedUsage does, so the marker must keep the full baseline. buildAbortedResponseMetadata now subtracts nothing. |
||
|
|
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. |
||
|
|
7c071e244b
|
🔢 fix: Prevent "approximately" tildes from rendering as markdown subscript (#13743)
* 🔢 fix: Prevent "approximately" tildes from rendering as markdown subscript `remark-supersub` splits text nodes on every `~`; an even number of tildes wraps the in-between text in `<sub>`. "Approximately" usage like `~50% ... ~10%` pairs up and subscripts everything between the two tildes. A backslash escape cannot fix this: micromark resolves `\~` back to a bare `~` before supersub runs. Instead, `preprocessTilde` swaps approximation tildes (a `~` prefixing a number, not attached to a word) for the Unicode tilde operator `∼` (U+223C), which renders as a tilde but is not split by supersub. Mirrors `preprocessLaTeX`: early return, single regex pass, code-region skipping. Genuine subscripts (`H~2~O`, `a ~2~ b`), strikethrough, escaped tildes, and home paths are preserved. * 🔢 fix: Harden tilde preprocessing — escaped tildes, URLs, math, MarkdownLite Addresses Codex review findings: - Convert escaped approximation tildes too (`\~50%`): markdown decodes `\~` to `~` before supersub, so the escape still pairs into a subscript. - Anchor matches to a prose boundary (start / whitespace / open bracket) and exclude `\(`/`\[`/`\{`, so URL path tildes (`/~50`) and math delimiters (`$~10$`, `$$~10$$`, `\(~10\)`) are left untouched. - Apply preprocessTilde in MarkdownLite (user messages + search/subagent/ code-analysis displays), which also enables remark-supersub. * 🔧 refactor: Neutralize approximation tildes via remark plugin, not raw-text Replaces the string-level preprocessTilde with `remarkApproxTilde`, a remark plugin that rewrites "approximately" tildes (`~50%` → `∼`) on parsed text nodes before remark-supersub runs. Because it operates on the AST, code spans, fenced code blocks (backtick *and* ~~~), inline code with any backtick count, link destinations, and math spans are structurally excluded — none are `text` nodes — resolving every raw-text edge case Codex flagged without region-scanning heuristics. Escaped `\~` is covered for free (markdown decodes it before the plugin runs). - New client/src/utils/tilde.ts: `normalizeApproxTildes` (pure, per-text-node) + `remarkApproxTilde` plugin. - Wired into both renderers (markdownConfig + MarkdownLite), before supersub. - latex.ts / Markdown.tsx reverted to original; preprocessTilde removed. - tilde.spec.ts: pure-function cases + a hand-built-tree test proving code, math, and link URLs are untouched while text (incl. link text) is converted. * 🔧 fix: Cover quoted approximations and the markdown error-boundary fallback - Broaden the boundary to any non-word, non-tilde char (`(?<![\w~])`), which now includes quotes — `"~50%" ... "~10%"` was still subscripting because `"` was not a recognized prose boundary. Safe to widen because the plugin runs on text nodes, so code / links / math / URLs are already excluded structurally (the earlier allowlist only existed to dodge URL paths in raw text). - Add remarkApproxTilde to MarkdownErrorBoundary's fallback remark pipeline so the fix holds when a render error falls back to the minimal renderer. * 🔧 fix: Preserve autolink URL labels when normalizing tildes A GFM autolink renders the URL as its own label (a text node equal to the href), so the broadened boundary was rewriting `~50` inside a displayed URL to `∼50` even though the href stayed correct. Skip text nodes that are an autolink's label (value matches the destination, allowing for the implied scheme on www/email links), so the visible URL is preserved verbatim. Regular link labels (prose) are still normalized. Note: a single URL containing two `~<digit>` segments is still subscripted by remark-supersub itself — that's pre-existing behavior (reproduces with no plugin) and out of scope here. * 🔧 refactor: Drop unist-util-visit runtime dep from tilde plugin Replace the `unist-util-visit` import with a small self-contained recursive walk over text nodes (tracking the parent for the autolink-label check). This removes reliance on a transitively-hoisted runtime package — addressing the dependency- hygiene concern without adding a dependency or churning the lockfile. The type-only `unist` import remains (erased at build, no runtime resolution). Behavior is unchanged; verified against nested emphasis and list/paragraph trees. |
||
|
|
1ae54b39ad
|
🔍 fix: Render Web Search Favicons on Raw SERP Results During Streaming (#13741)
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
The streaming favicon stack was gated on `source.processed === true`, but the agents scrape pipeline marks sources processed only after a `Promise.all` barrier (the slowest page fetch). Raw SERP results — with everything needed to render favicons — arrive in the first attachment well before that, so the UI sat on "Searching the web" with no favicons for the entire scrape window. Render favicons from the raw sources as soon as they land instead of waiting for `processed`, filling the dead window and moving the label to "Processing results" immediately. Completed-state, turn scoping, and finalizing behavior are unchanged. |
||
|
|
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. |
||
|
|
4ee68d5240
|
💸 feat: Per-Agent Endpoint Token Config in Multi-Endpoint Billing (#13738)
* 💸 feat: Per-Agent Endpoint Token Config in Multi-Endpoint Billing
Price each collected/emitted usage item with the producing agent's resolved
endpoint token config, instead of the primary agent's for the whole graph.
Previously AgentClient.recordCollectedUsage and the subagent usage emitter used
a single this.options.endpointTokenConfig (the primary's) for every usage item.
A connected agent or subagent on a different custom endpoint that shares a model
id with an entry in the primary's tokenConfig was therefore mis-priced (a model
absent from it already fell back to the built-in rate map — no regression).
- Tag each usage with its producing agent: ModelEndHandler stamps
usage.agentId = agentContext.agentId; createSubagentUsageSink stamps the
child's subagentAgentId (UsageMetadata gains an optional agentId).
- buildAgentToolContext retains endpointTokenConfig so initialize.js can build
an agentId -> endpointTokenConfig map from agentToolContexts (the one map that
holds every agent, including pure subagents pruned from agentConfigs).
- AgentClient.resolveAgentEndpointTokenConfig(usage) looks up that map by
agentId, falling back to the primary config; used by both the billing path
(new optional resolveEndpointTokenConfig on recordCollectedUsage) and the
subagent cost emitter.
- recordCollectedUsage's resolver is optional and falls back to the batch
endpointTokenConfig, so the shared responses.js/openai.js call sites are
unchanged.
- Tests: two-endpoint graph with a colliding model id prices per-agent; resolver
nullish falls back to batch; subagent sink tags the child agent id.
* fix: Align emit-path cost with per-agent billing; honor known-agent built-in pricing
Addresses Codex review on the per-agent endpoint token config:
- Emit path (callbacks.js) now prices each on_token_usage event with the
producing agent's config (resolved via usageCost.resolveEndpointTokenConfig),
so streamed/persisted metadata.usage.cost matches the per-agent balance
transaction. The agentId tag is resolved server-side and stripped from the
emitted/persisted payload.
- Resolver (resolveAgentTokenConfig) now treats a known agent's config as
authoritative, including undefined → built-in pricing, so a known non-custom
agent in a custom-primary graph is no longer charged the primary's rates.
Only untagged/unknown usage falls back to the primary config.
- endpointTokenConfigByAgentId records every known agent (value may be
undefined) so the resolver distinguishes known-no-rates from unknown.
|
||
|
|
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. |
||
|
|
98704f28c1
|
🌐 fix: Centralize Outbound Proxy Handling (#13726)
* fix: centralize outbound proxy handling * chore: sort proxy imports * test: update proxy helper mocks * fix: honor proxy bypasses consistently * fix: support http axios proxy targets |
||
|
|
16bbc4b97e
|
🎨 fix: Gate message hover-reveal controls on hover capability, not width (#13712)
Apply the MessageTimestamp transform to all remaining hover-reveal controls in the message UI: replace the md: breakpoint proxy with a (hover: hover) media query so action buttons stay visible on touch and other non-hover devices (e.g. tablets wider than md), while still hiding until row hover/focus where a pointer supports it. The group-hover:visible reveal trio keeps no hidden base state, so it only drops the md: prefixes; the actual hide-until-hover mechanism is the opacity variant, which stays focusable and in the accessibility tree while hidden. |
||
|
|
fc2ae89aa6
|
⌚ feat: Show Message Timestamps on Hover (#13709)
* ✨ feat: Show Message Timestamps on Hover Reveal a message's time inline next to the author name on hover. Recent messages (under 24h) show a relative time ("10 minutes ago") with the absolute date on hover; older messages show the absolute date directly. A shared MessageTimestamp component is used by both MessageRender and ContentRender, with createdAt added to their memo comparators so the timestamp appears once it's available. Resolves #5199 * 🎨 fix: Gate message timestamp reveal on hover capability, not width Use a (hover: hover) media query instead of the md: breakpoint so the timestamp stays visible on touch and other non-hover devices (e.g. tablets wider than md), while still revealing on hover/focus where a pointer supports it. * 🎨 fix: Show message timestamps across all renderers and keep them live Extend the hover timestamp to the Assistants (MessageParts), shared-link, search, and parallel/multi-response renderers so every prompt and response shows when it was sent. The parent message's createdAt is threaded down to parallel column headers (SiblingHeader). Add a shared, ref-counted minute ticker (useTimeTick) so relative labels like "2 minutes ago" stay current while a conversation is left open instead of freezing at first render. |
||
|
|
9618be6eb3
|
🌿 fix: Preserve Viewed Branch on Sibling-Tree Churn (#13732)
* 🌿 fix: Preserve Viewed Branch on Sibling-Tree Churn Regenerating a message could snap the view to an unrelated newest branch. MultiMessage reset siblingIdx to 0 (newest) on any messagesTree.length change, but getRegenerateSubmissionMessages slices the flat message array during a regenerate — the streaming handlers render a tree missing unrelated sibling branches, then finalHandler restores the full set. That 2→1→2 child-count swing snapped unrelated forks to their newest sibling, so regenerating the latest response on an older branch jumped to a previously regenerated branch. Replace the indiscriminate reset with per-fork branch memory: a 'seen' set distinguishes a genuinely new sibling (submission/regeneration/edit here — focus it) from one transiently dropped and restored (preserve the user's branch). Decision extracted as the pure, unit-tested resolveSiblingSelection. - client/src/utils/messages.ts: resolveSiblingSelection + tests - MultiMessage: seen/selectedId refs, structural id-signature effect - e2e: regenerate-latest-on-older-branch keeps the viewed branch (fails on the old reset, passes now) * 🧪 test: Long-Thread Branch Preservation E2E Add the user-reported scenario: in a multi-turn thread, regenerate an earlier response (forking a root branch), switch back to the original, then regenerate a later response on it — the original branch must stay intact. Uses labeled prompts so each turn's unique reply is a reliable settle signal. Verified it fails on the original MultiMessage and passes with the fix. * 🎨 style: Fix import order in MultiMessage (react before recoil) * 🌿 fix: Keep Unrelated Branches in Regenerate Optimistic Render Regenerating a message used a flat `messages.slice(0, targetIndex)` for the optimistic render, which also drops unrelated sibling branches that merely sit later in the flat array. Mid-regenerate the thread briefly collapsed to a short branch (visible flash) and the scroll jumped to the shrunken content and didn't recover — the same flat-array root cause as the branch-reset bug. Remove only the regenerated response and its descendants, keeping unrelated branches. The thread (and scroll) stay put through the regenerate. This array is render-only — the server regenerates from parentMessageId and createPayload doesn't include it — so summing by subtree never affects the request. Verified via a small-viewport scroll trace: old collapses 903->295px / 8->2 renders mid-stream; fixed stays 903px / 8 renders, scroll held at bottom. Unit test covers the keep-unrelated-branches behavior (fails on the old slice). * 🌿 fix: Let an Explicit Branch Selection Survive Streaming ID Churn resolveSiblingSelection focused any unseen sibling id before checking the committed selection. When an in-flight response's id is replaced mid-stream (placeholder → server/run id, e.g. useStepHandler re-keys to runId) after the user switched to a different sibling, that swap looked like a brand-new sibling and stole focus back to the streaming branch. Reorder: the committed selection wins while still present; only focus a fresh sibling when the selection is gone (regenerated away, or its own placeholder id was just replaced — that's how a regen/edit still takes focus, since the slice removes the old response). Added unit tests for both churn directions. * 🌿 fix: Only Focus a New Sibling When the Fork Actually Grew The previous churn fix (selection-wins-first) was too aggressive: a genuinely new sibling ADDED while the prior selection is still present — e.g. a follow-up re-parented as a sibling after a generation-start failure — was no longer focused, so its reply never rendered (broke message-tree generation-start recovery e2e). Gate new-sibling focus on actual growth: resolveSiblingSelection now takes prevCount and only focuses a never-seen id when ids.length > prevCount. A same-count placeholder→server id swap (churn) or a restored already-seen sibling is not growth, so the committed selection still wins there. Covers follow-up/new-branch focus, churn steal-prevention, and self-churn follow. message-tree + chat e2e: 17 passed (incl. the recovered generation-start test). * 🌿 refactor: Drop MultiMessage Branch-Memory in Favor of the Slice Fix The regenerate-slice fix (keep unrelated branches in the optimistic render) is the true root cause: with no spurious tree collapse, the original setSiblingIdx(0)-on-length-change never misfires, so the branch-reset is fixed without per-fork memory. The earlier MultiMessage rewrite (seen/selectedId/ prevCount + resolveSiblingSelection) was a symptom patch added before the root cause was found, and its per-instance memory generated two edge-case findings (placeholder→server id churn; divergence from external siblingIdx writes like resume restore). Revert MultiMessage to the simple upstream version and remove resolveSiblingSelection (+ its tests). The slice fix + the existing branch e2e (chat.spec: switch-back, regenerate-latest, long-thread) cover the behavior; all 17 chat + message-tree branch specs pass with this version. * 🌿 fix: Focus the Regenerated Response When Its Fork Count Is Unchanged When a parent already has multiple sibling responses and the user switches to a non-latest one and regenerates it, the optimistic slice drops the target but keeps the other siblings, so the child count is unchanged. MultiMessage only resets the (reversed) sibling index on a length change, so the stale index kept pointing at the kept sibling and the regenerating response stayed hidden until the server restored the dropped sibling at finalize (count bump → reset). Explicitly focus the newest sibling (reversed index 0 = the appended response) of the regenerated fork in createdHandler. Position-based, fires only on the regenerate action, so it doesn't reintroduce the placeholder→server id churn or external-write fragility that a per-render selection memory had. E2E: new during-stream test (slow+counted reply marker) asserting the regenerating response is visible before finalize; negatively verified (fails without the focus call, passes with it). * 🌿 fix: Eliminate Pre-Created Flash by Focusing at the Optimistic Render The createdHandler focus removed the until-finalize bug, but a brief flash remained between clicking regenerate and the `created` event: useChatFunctions renders the optimistic placeholder first, and that render has the same unchanged-count problem, so the kept sibling showed until createdHandler fired. Extract the focus into a shared useFocusRegeneratedResponse hook and apply it at the optimistic render too (useChatFunctions) and on `created` (useEventHandlers). The placeholder is now focused from the first frame. E2E: gated pre-created test — holds the SSE stream GET (the chat POST returns a stream id; the stream is a separate GET) so `created` cannot arrive, leaving only the optimistic render, then asserts the kept sibling is already gone. This isolates the optimistic focus (createdHandler cannot mask it); negatively verified (fails without the optimistic focus call). * 🧪 test: Extend Store Mock for the Regenerate Focus Hook useChatFunctions.regenerate.spec.tsx mocks ~/store and recoil partially; the new useFocusRegeneratedResponse calls store.messagesSiblingIdxFamily via a recoil `set`, neither of which the mock provided (TypeError on regenerate). Add messagesSiblingIdxFamily to the store mock and `set` to the useRecoilCallback mock. Test-only; production code unchanged. |
||
|
|
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
|
||
|
|
3c3837bb7d
|
🧾 fix: Bill Subagent Child-Run Model Usage in Parent Transactions (#13683)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
* 🧾 fix: Bill Subagent Child-Run Model Usage in Parent Transactions * 🩹 fix: Type Subagent Usage Sink Structurally Until SDK Release * 🔧 chore: Update @librechat/agents dependency to version 3.2.35 in package-lock.json and related package.json files |