Commit graph

4210 commits

Author SHA1 Message Date
Danny Avila
b570ce4cd7
🐳 ci: Build Docker Client Package With Data Provider Dist (#13097) 2026-05-12 15:24:41 -04:00
Danny Avila
6b5596ec36
🍪 refactor: Refresh CloudFront Media Cookies (#13091)
* fix: refresh CloudFront media cookies

* fix: satisfy changed-file lint

* fix: centralize CloudFront image retry

* fix: honor base path for CloudFront refresh

* fix: bypass auth refresh for CloudFront cookie retry

* fix: pass app auth header to CloudFront retry

* test: cover CloudFront refresh with OpenID reuse

* fix: avoid duplicate CloudFront refresh retries

* fix: clear CloudFront scope cookie with matching flags
2026-05-12 13:26:05 -04:00
Ravi Kumar L
05d4e90f91
🌩️ feat: Strict CloudFront signed cookie enforcement via requireSignedAccess (#13078)
* feat(cloudfront): add requireSignedAccess to enforce strict signed access

Introduces cloudfront.requireSignedAccess (default false). When enabled,
initializeCloudFront requires both CLOUDFRONT_KEY_PAIR_ID and
CLOUDFRONT_PRIVATE_KEY, rejects the unimplemented imageSigning="url"
mode, and initializeFileStorage throws to block startup on any
CloudFront init failure. OSS path is unchanged: missing keys still
log-and-continue when requireSignedAccess is false.

Adds low-noise startup and cookie-issuance logs without leaking signed
URLs, policies, signatures, private keys, or cookie values.

* fix(cloudfront): reject requireSignedAccess unless imageSigning is "cookies"

Previously requireSignedAccess=true was accepted with imageSigning="none"
or "url", but setCloudFrontCookies() only runs for "cookies" — leaving
strict mode toothless: CloudFront stayed publicly accessible, or image
delivery broke on a distribution that actually requires signed access.

Adds a Zod refinement plus a runtime guard in initializeCloudFront so
the only currently-functional strict configuration is imageSigning
"cookies". Signed URL mode can lift this restriction once implemented.

* fix(cloudfront): resolve strict access type checks

* chore(cloudfront): reduce strict startup log noise

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-05-11 23:30:01 -04:00
Danny Avila
7b9a57a467
🛡️ fix: Harden OpenID Session Token Reuse (#13086)
* fix: Harden OpenID Session Token Reuse

* fix: Preserve OpenID Session Token On Forced Refresh

* fix: Gate Preserved OpenID Id Token By Expiry

* test: Cover OpenID Id Token Expiry Buffer
2026-05-11 23:29:01 -04:00
Danny Avila
17a08224e1
🍪 fix: Refresh CloudFront Cookies On Auth Refresh (#13083)
* fix: Refresh CloudFront Cookies On Auth Refresh

* fix: Exclude Federated Tokens From Refresh Lookup
2026-05-11 22:33:27 -04:00
github-actions[bot]
929082387f
🌍 i18n: Update translation.json with latest translations (#13080)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-11 21:15:53 -04:00
Danny Avila
3e7262cfe0
📦 chore: Bump @librechat/agents to v3.1.85 and mermaid to v11.15.0 (#13079)
* 📦 chore: Update @librechat/agents to version 3.1.85 in package-lock.json and package.json files

* 📦 chore: Update mermaid to version 11.15.0 in package.json and package-lock.json
2026-05-11 19:14:18 -04:00
Danny Avila
0a7255b234
🎭 feat: Support OpenID Audience On Refresh Grants (#13077) 2026-05-11 17:40:30 -04:00
Danny Avila
36e95353ed
🏷️ refactor: Rename Code Interpreter Labels To Run Code (#13071)
* fix: Rename Code Interpreter UI to Run Code

* fix: Remove unused Run Code i18n keys

* fix: Restore tool call labels

* fix: Keep assistant Code Interpreter copy

* fix: Update agent code environment copy

* fix: Update code environment upload copy

* fix: Use fresh run code locale keys

* fix: Update code environment test copy

* fix: Sort upload translation test keys
2026-05-11 16:24:33 -04:00
Danny Avila
c385f2ba88
📦 feat: Configure Skill Import Size Limit (#13073)
* fix: configure skill import size limit

* fix: validate skill import size in ui

* fix: align skill import size boundary

* fix: show exact skill import limit
2026-05-11 16:24:04 -04:00
Danny Avila
8735c1763c
🧵 fix: Preserve Upload Context Across Multipart Routes (#13072)
* fix skill multipart imports under strict isolation

* fix file upload context after multipart parsing

* fix skill upload tenant resolution

* fix rejected upload cleanup
2026-05-11 15:46:48 -04:00
Danny Avila
b32b328b87
🛡️ fix: Harden Artifact Routing Lookups (#13069) 2026-05-11 15:42:44 -04:00
Odrec
0f59cd79dd
🪙 fix: Pass appConfig to getBalanceConfig in set-balance script (#13070)
`config/set-balance.js` calls `getBalanceConfig()` without an argument,
so it cannot read the parsed `balance` section from librechat.yaml.
As a result the script always errors with "Balance is not enabled.
Use librechat.yaml to enable it" — even when balance is enabled.

`config/add-balance.js` already follows the correct pattern (fetch
`appConfig` via `getAppConfig()` and pass it into `getBalanceConfig`),
introduced in #9234. This applies the same pattern to set-balance.js.

Co-authored-by: Odrec <Odrec@users.noreply.github.com>
2026-05-11 15:08:23 -04:00
Danny Avila
0449c423a2
🗝️ fix: Enforce Skill Share Role Permission (#13062)
* fix: enforce skill share role permission

* fix: preserve share capability bypass

* refactor: move share policy middleware to api package

* style: order share middleware imports

* fix: satisfy share middleware type checks

* test: cover share policy resource types
2026-05-11 09:39:58 -04:00
Danny Avila
7631366f52
🪵 chore: Log Subagent Limit Hits (#13068) 2026-05-11 09:25:08 -04:00
Danny Avila
70b6bb69d3
🧬 fix: Bound Subagent Expansion (#13064)
* fix: Bound subagent expansion

* fix: Preserve subagent path depth
2026-05-11 08:53:53 -04:00
github-actions[bot]
508168fa57
🌍 i18n: Update translation.json with latest translations (#13058)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-11 08:41:16 -04:00
Danny Avila
9797c85e23
🧮 fix: Count Rejected Skill Import Bytes (#13063) 2026-05-11 08:40:31 -04:00
Danny Avila
822ad6c36a
🧯 fix: Bound Permission Superset Cache Inputs (#13065) 2026-05-11 08:39:37 -04:00
Danny Avila
52ccb1379b
🪪 refactor: Require Remote OIDC Audience for Agents API OAuth (#13066) 2026-05-11 08:38:13 -04:00
Danny Avila
7129b1b1e4
📜 refactor: Improve Skill Handling Logs (#13057)
* refactor: Streamline batch upload error handling in `uploadCodeEnvFile`
* refactor: Enhance session info error logging in `getSessionInfo`
* refactor: Update error logging to use `logAxiosError` in various agent handlers and skill file processing functions
* refactor: Consolidate missing resource checks in `createToolExecuteHandler` for better clarity
2026-05-11 02:15:51 -04:00
Danny Avila
763fab2e3e
🛂 fix: Restrict OpenID JWT Bearer Reuse (#13052)
* fix: Restrict OpenID JWT bearer reuse

* fix: Require signed OpenID reuse cookie

* fix: Bind OpenID reuse to authenticated user
2026-05-11 02:00:38 -04:00
Danny Avila
846eb0aa2c
🛰️ fix: Validate Vertex Endpoint Overrides (#13054)
* fix: Validate Vertex endpoint overrides

* fix: Allow Vertex PSC endpoint overrides

* fix: Allow restricted Vertex PSC endpoints
2026-05-11 01:11:14 -04:00
Danny Avila
5bab22d236
🛡️ fix: Gate Bash PTC Capabilities (#13053) 2026-05-10 21:23:02 -04:00
Danny Avila
030dc98a1d
☁️ fix: Enable Azure Agent Provider Uploads (#13045) 2026-05-10 17:47:05 -04:00
Danny Avila
1e9d0cbd0d
🖥️ style: Render Bash PTC Calls With Bash UI (#13046)
* fix: Render bash PTC calls with bash UI

* fix: Group bash execution tools consistently
2026-05-10 14:09:04 -04:00
Danny Avila
c3ec23f9b8
🌐 feat: Support Vertex AI Multi-Region Endpoints (#13044)
* feat: support Vertex AI multi-region endpoints

* fix: sync Vertex endpoint with final location
2026-05-10 13:41:58 -04:00
Danny Avila
8fc68ebac0
🧬 refactor: Align OpenRouter Reasoning Payloads (#13039)
* fix: Align OpenRouter reasoning payloads

* test: Update OpenRouter reasoning expectations

* fix: Preserve xhigh for future Claude models

* fix: Preserve OpenRouter Responses verbosity

* test: Type OpenRouter verbosity fixture

* fix: Preserve custom verbosity values
2026-05-09 21:04:21 -04:00
Danny Avila
715a4a5fc1
🧰 refactor: Use Bash PTC for Agent Tools (#13042)
* fix: Use Bash PTC for programmatic agent tools

* fix: Preserve legacy PTC event calls
2026-05-09 16:31:09 -04:00
Danny Avila
94ac44e3b4
🛡️ chore: Harden Docker Dev Image Builds (#13041)
* chore: harden Docker dev image builds

* chore: align Docker dev workflow timeouts
2026-05-09 16:30:28 -04:00
Danny Avila
2e683f112b
🦘 fix: Skip OpenAI Model Fetch For User-Provided Keys (#13038)
* fix: skip OpenAI model fetch if using user-provided key

There was a check present (via `opts.userProvidedOpenAI`), but it wasn't
working because `loadDefaultModels()` doesn't provide that parameter. As a
result, the server would repeatedly try to request models from OpenAI and get
401 errors in return.

We now check the env var directly, which matches how
`getAnthropicModels()` works.

* chore: remove unused OpenAI model option

* fix: honor explicit OpenAI key for model fetch

* fix: fall back from empty OpenAI option key

---------

Co-authored-by: Dan Lew <daniel@mightyacorn.com>
2026-05-09 16:12:25 -04:00
Danny Avila
80ce956c94
📜 fix: Scope Read File Prompt For Code Agents (#13040)
* fix: Scope read_file prompt for code agents

* fix: Align code read_file prompt behavior
2026-05-09 16:09:56 -04:00
Danny Avila
c67e2b54dc
🔐 feat: Mint Code API Auth Tokens (#13028)
* feat: Mint CodeAPI auth tokens

* style: Format CodeAPI download route

* fix: Prune CodeAPI token cache

* fix: Propagate CodeAPI managed auth

* test: Mock CodeAPI auth in traversal suite

* fix: Pass auth context to invoked skill cache

* feat: Mint CodeAPI plan context

* chore: Refresh CodeAPI auth guidance

* fix: Guard OpenID JWT fallback

* fix: Default CodeAPI JWT tenant in single-tenant mode

* chore: Update @librechat/agents to version 3.1.84 in package-lock.json and package.json files

* chore: Standardize references to Code API in comments and tests
2026-05-09 16:09:10 -04:00
Danny Avila
8a654dc8b1
🧭 feat: Add OpenRouter Prompt Cache Setting (#13029)
* feat: add OpenRouter prompt cache setting

* fix: type OpenRouter schema lookup

* fix: honor proxied OpenRouter prompt cache

* refactor: flatten endpoint schema fallback

* chore: Bump `@librechat/agents` to version 3.1.82

* fix: Default OpenRouter prompt cache params

* test: Align OpenRouter config expectations

* test: Update OpenRouter default cache expectation

* fix: Align OpenRouter Detection

* chore: Bump `@librechat/agents` to version 3.1.83

* docs: Remove OpenRouter prompt cache setup note

* refactor: Use provider enum for OpenRouter defaults

* style: Format OpenRouter defaults guard
2026-05-09 11:46:09 -04:00
Dustin Healy
0d5c2b339a
🛟 fix: Allow Empty modelSpecs.list to Unstick Admin-Panel Saves (#13036)
* 🛟 fix: Allow empty modelSpecs.list to unstick admin-panel saves

The unconditional `.min(1)` on `specsConfigSchema.list` rejected an empty
list even when `enforce: false`, leaving admin panels (which save fields
path-granularly) with no atomic way to clear the list once it had been
populated. Once an admin reached `list: [entry]` and deleted the only
entry, every subsequent save failed schema validation and the section
became stuck.

Relax the schema to `.default([])`. The `.min(1)` was added in #5218 as
part of bundled cleanup, not as a deliberate rule. Every consumer of
`modelSpecs.list` already handles the empty/undefined case (`?.list`,
`?? []`, length-checked), and `processModelSpecs` short-circuits to
`undefined` when the list is empty so the runtime treats it as "no
specs configured." No call site is load-bearing on length >= 1.

Tighten the `buildEndpointOption.js` enforce guard from
`?.list && ?.enforce` to `?.list?.length && ?.enforce`. Empty arrays
are truthy in JS, so the existing guard would have entered the enforce
branch on `list: []` and returned "No model spec selected" or "Invalid
model spec" had `processModelSpecs` ever been bypassed.

Add a runtime warn in `processModelSpecs` when `enforce: true` is
configured alongside an empty list, so operators see the resulting
"enforcement disabled" state in logs rather than silently getting a
permissive runtime.

Add coverage for the empty-list parse path in `config-schemas.spec.ts`
and for the empty-list-with-enforce branch in `buildEndpointOption.spec.js`.

* chore: update import order in config-schemas.spec.ts
2026-05-09 11:39:15 -04:00
Danny Avila
ac3600cdd7
🗂️ fix: Remove Generated Code Files From Prompt Context (#13037) 2026-05-09 11:38:53 -04:00
Danny Avila
c7a4e6d418
📦 chore: Bump @babel/preset-env to v7.29.5 (#13034) 2026-05-08 19:51:06 -04:00
Danny Avila
b922187abb
🛟 fix: Summarization Provider misses vertexai + case-mismatched custom endpoints (#13025)
`resolveSummarizationProvider` calls `getProviderConfig` to translate the
agent's resolved provider into an initializer + client overrides. Three
real-world inputs were unsupported and fell through to "raw provider"
fallback (silently dropping client overrides):

1. **`vertexai`** — not in `providerConfigMap` at all. Vertex shares
   initialization with Google (auth-only runtime distinction). Map
   `Providers.VERTEXAI` to `initializeGoogle`.

2. **`openrouter` (and other known custom providers) with CamelCase
   custom endpoint names** — agent main flow looks up endpoints
   case-sensitively (case-preserving keys are how
   `loadCustomEndpointsConfig` lets users have distinct entries
   differing only in case). Once it succeeds, `agent.provider` is
   normalized to lowercase. Downstream resolvers re-enter
   `getProviderConfig` with the lowercased value and miss configs
   whose `name` is camel-cased. Add a case-insensitive fallback,
   narrowly scoped to known custom providers and only after the
   case-sensitive direct lookup fails.

3. **Ambiguous case-insensitive matches (codex review feedback)** —
   if the user has e.g. `OpenRouter` and `OPENROUTER` (neither
   lowercase) and the agent runtime passes `openrouter`, the
   case-insensitive fallback could silently route to whichever entry
   appears first in the array (potentially different baseURL/apiKey).
   Detect multiple case-insensitive matches and throw a clear error
   with both names rather than picking arbitrarily.

## Tests

`providers.spec.ts` — new file, 7 tests:
- vertexai → Google initializer
- google (API key) → Google initializer (regression guard)
- case-insensitive fallback when only CamelCase entry exists
- exact-case match preserved when both casings exist (case identity)
- exact-case lowercase entry still resolves
- throws on ambiguous case-insensitive matches when no exact-case exists
- still throws when no match at all
2026-05-08 18:52:01 -04:00
Danny Avila
d90567204e
🛟 fix: persist Vertex Gemini 3 thoughtSignatures across DB round-trips (#13026)
When a tool round-trip is interrupted between the tool result and the
model's text reply (user aborted, network drop, pod restart, ...) and
LibreChat persists the partial assistant message, the next conversation
turn reconstructs an `AIMessage` from `formatAgentMessages` that has
`tool_calls` populated but no `additional_kwargs.signatures`. Vertex
Gemini 3 rejects the resumed request with 400 because the most recent
historical functionCall has no `thought_signature`.

## Storage shape

Capture as `Record<tool_call_id, signature>` rather than a flat array.
This addresses the codex P1 review:

  > When an assistant turn contains multiple sequential tool-call batches,
  > this restoration path writes all persisted thoughtSignatures onto only
  > the last tool-bearing AIMessage. Vertex/Gemini validates signatures
  > for each step in the current tool-calling turn, so earlier
  > functionCall steps reconstructed without their signature can still
  > fail with 400.

A single agent run can fire multiple `chat_model_end` events when the
loop cycles the LLM with intervening tool results — each cycle owns a
distinct `tool_call_id`. Per-id storage maps each signature back onto
the right reconstructed `AIMessage`, not just the last one.

## Mapping

`additional_kwargs.signatures` is a flat array indexed by *response part*
(text + functionCall interleaved). `tool_calls` is just the function
calls in their original order. Non-empty signatures correspond 1:1 with
tool_calls in order — see `partsToSignatures` in
`@langchain/google-common`. Single-pass walk maps `signatures[i]` (when
non-empty) onto the i-th `tool_call.id`.

## Pipeline

| Stage | File | Change |
|---|---|---|
| Capture | callbacks.js | `ModelEndHandler` accepts `Record<string,string>` map; walks signatures + tool_calls in tandem to record per-id. Gated on the map being provided — non-Vertex flows are no-op (and also no-op even when provided, since they don't emit signatures). |
| Plumbing | initialize.js | Allocate `collectedThoughtSignatures = {}`, share with handler + client. Always allocated; the JSDoc explicitly documents that it stays empty for non-Vertex providers. |
| Surface | client.js | `sendCompletion` returns `metadata.thoughtSignatures` when the map has entries; falls through unchanged when empty. |
| Persist | (existing BaseClient.handleRespCompletion) | Writes `metadata` from `sendCompletion` onto `responseMessage.metadata`. Mongoose `Mixed` — no migration. |
| Restore | formatMessages.js | Track every tool-bearing AIMessage produced from a TMessage. For each, build a position-aligned `additional_kwargs.signatures` array (empty placeholders for tool_calls without a stored sig). Agents' `fixThoughtSignatures` dispatches non-empty entries to functionCall parts in order. |

## Live verification

- **Single-step:** real Vertex `gemini-3.1-flash-lite-preview` resume-after-tool case. With fix  / without  400.
- **Multi-step (codex case):** real two-step agent loop (list /tmp → echo done). Each step's signature attaches to its own reconstructed AIMessage. With fix  / without  400.
- **Cross-provider:** Anthropic Claude haiku-4.5 + OpenAI gpt-5-mini accept the persisted/restored shape unchanged.

## Tests

`modelEndHandler.spec.js` (new) — 6 tests:
- maps non-empty signatures onto tool_call_ids in order
- accumulates per-id across multiple `model_end` events (multi-step)
- no-op when `collectedThoughtSignatures` is null
- no-op when `signatures` field missing (non-Vertex)
- no-op when `tool_calls` missing
- preserves existing `collectedUsage` array contract

`formatAgentMessages.spec.js` — 6 new tests:
- restores onto the AIMessage that owns the tool_call
- per-step attachment for multi-step turns (codex review case)
- preserves tool_call ordering when signatures are partial
- no-op when metadata.thoughtSignatures absent
- no-op when assistant has no tool_calls
- no-op when stored ids don't match any current tool_call

37 passing across 3 suites; 15 existing formatAgentMessages tests unchanged.

## Compatibility

- Backward-compatible — restore gated on `metadata.thoughtSignatures` being a populated object; capture gated on the map being provided.
- No schema migration — uses `Message.metadata: Mixed` already in place.
- Cross-provider safe — non-Vertex providers tolerate the field (verified live against Anthropic + OpenAI converters).
- Pairs with [agents#159](https://github.com/danny-avila/agents/pull/159) for full coverage on histories that mix plain-text and toolcall AIMessages.
2026-05-08 18:51:34 -04:00
Vinicius Dittgen
a565a61a23
⛴️ fix: Use Bitnami Legacy MongoDB Image in Helm Chart (#13032)
Bitnami moved versioned image tags from docker.io/bitnami to
docker.io/bitnamilegacy on 2025-08-28, which causes ImagePullBackOff
on a fresh Helm install of the LibreChat chart. Override the MongoDB
subchart image repository to bitnamilegacy/mongodb so installs
succeed out of the box.

Fixes #13031
2026-05-08 18:21:34 -04:00
Dustin Healy
e262219c8f
🔄 feat: Cross-Origin Admin OAuth Refresh (#13007)
* feat(admin-panel): add /api/admin/oauth/refresh endpoint for cross-origin BFF refresh

The cookie-based /api/auth/refresh controller can't be reached cross-origin
from a separately-hosted admin panel because the refresh-token cookie isn't
sent on cross-origin fetches. Add a dedicated POST /api/admin/oauth/refresh
endpoint that accepts the refresh token in the request body, exchanges it
at the IdP via openid-client refreshTokenGrant, and returns the same
response shape as /api/admin/oauth/exchange.

Implementation lives in packages/api/src/auth/refresh.ts as the
applyAdminRefresh helper. It validates the refreshed tokenset, looks up the
admin user by openidId (with optional user_id disambiguation when multiple
user docs share an openidId), mints the bearer via an injected mintToken
hook, and runs an optional onRefreshSuccess hook for downstream forks that
need to update server-side session state.

The default mintToken passed by the OSS route signs an HS256 LibreChat JWT
via generateToken so admin panel callers continue to use the existing local
JWT strategy. Forks that prefer to hand back an IdP-signed token (e.g. for
deployments where the JWT auth gate is JWKS-only) override mintToken
without changing the helper or the route.

Also threads expiresAt through AdminExchangeData and AdminExchangeResponse
so admin panel clients can drive proactive refresh before the bearer
expires. Defaults the OSS exchange flow to Date.now() + sessionExpiry.

* fix(admin-panel): address review feedback on /api/admin/oauth/refresh

mintToken now returns {token, expiresAt} so the minter is authoritative
for the bearer's lifetime instead of deriving it from the IdP `exp` claim.
The refresh response would otherwise lie to the admin panel and trigger
premature or late refresh cycles.

The helper now falls back to the inbound refresh_token when the IdP omits
one on rotation (Auth0 with rotation off, Microsoft personal accounts).
Without this the admin panel loses its refresh capability after one cycle.

Other hardening:

resolveAdminUser validates user_id with Types.ObjectId.isValid before
hitting Mongoose, avoiding a CastError that would surface as a generic
500 with no useful information for the client.

If user_id resolves to a user whose openidId does not match the refreshed
sub, throw USER_ID_MISMATCH (401) instead of silently swapping in a
different user matching the sub.

Wrap tokenset.claims() in readClaims so an IdP that returns a tokenset
without a usable id_token gets mapped to CLAIMS_INCOMPLETE (502) rather
than bubbling a raw exception.

findUsers now uses the same SAFE_USER_PROJECTION as getUserById so the
fallback path no longer pulls password/totpSecret/backupCodes into memory.

Removed dead fields (email on AdminRefreshClaims, id_token on
RefreshTokenset) and fixed import ordering per AGENTS.md.

Adds packages/api/src/auth/refresh.spec.ts: 18 tests covering the happy
path, userId disambiguation (match, invalid ObjectId, null, mismatch),
all error branches (IDP_INCOMPLETE, CLAIMS_INCOMPLETE for both throw and
missing sub, USER_NOT_FOUND, mintToken/onRefreshSuccess propagation), and
refresh-token preservation under rotation/no-rotation.

* chore(admin-panel): polish per re-review on /api/admin/oauth/refresh

readClaims now logs the original error name/message at warn before mapping
to CLAIMS_INCOMPLETE so a programming bug doesn't get silently rebadged
as an IdP problem in production logs.

The route handler's JSDoc now enumerates every error response (status +
error_code) so admin-panel implementors can plan for each branch without
reading the source.

Tightens the helper's surface: removed the now-dead `exp` field from
`AdminRefreshClaims` (only `sub` is read since the v2 mintToken refactor),
and tightened `AdminRefreshDeps.findUsers`'s projection parameter from
`string | null` to `string` so the contract matches actual usage.

Test polish: the userId-resolves-to-null fallthrough test now asserts the
exact `findUsers` and `getUserById` call arguments so a regression in the
fallthrough query shape is caught. The "skips onRefreshSuccess" test now
asserts a populated response shape rather than just `toBeDefined`.

Declined per prior triage and re-confirmed: a role guard inside
`applyAdminRefresh` (downstream `/api/admin/*` already enforces
ACCESS_ADMIN via requireCapability) and moving the IdP grant call out of
the JS route into TypeScript (matches existing oauth.js / openidStrategy
pattern; package-boundary refactor belongs in a separate PR).

* fix(admin-panel): reject /api/admin/oauth/refresh tokensets from foreign issuers

When the route handler can resolve the configured OpenID issuer, it now
threads it into applyAdminRefresh as expectedIssuer. The helper compares
that against the tokenset claims iss (after normalizeOpenIdIssuer on
both sides to absorb trailing-slash differences) and throws
ISSUER_MISMATCH (401) on mismatch.

The check is skipped when either side is unset so behavior is unchanged
for IdPs that don't return iss on a refresh-grant id_token, and for
older deployments where the OpenID config doesn't expose serverMetadata.

This is a defense-in-depth measure for the refresh path only. The
deeper OIDC posture fix (binding IUser lookup to (sub, iss) as a pair)
is pre-existing debt across openidStrategy.js and the regular exchange
flow as well, and belongs in a separate PR with the schema change and
backfill migration.

* fix(admin-panel): bind refresh user lookup to (sub, iss) and handle getOpenIdConfig throw

Two fixes raised on the PR thread that I previously misdescribed:

The user lookup in resolveAdminUser was keyed on openidId alone, so a
tokenset from a different issuer that happened to share the same sub
could resolve to a local user from a different IdP. Now exports
getIssuerBoundConditions and isUserIssuerAllowed from openid.ts (the
helpers findOpenIDUser already uses) and reuses them. The findUsers
filter becomes ($or of getIssuerBoundConditions for openidId) when an
expectedIssuer is provided, with the same legacy backward-compat
clause for users whose openidIssuer field was never populated. The
direct user_id path now also checks isUserIssuerAllowed and throws
USER_ID_MISMATCH if the stored openidIssuer disagrees with the
configured issuer.

The route's getOpenIdConfig() call was previously documented as
returning null when uninitialized; the actual implementation throws.
That made the if (!openIdConfig) guard unreachable, and an unconfigured
server would surface as 500 INTERNAL_ERROR rather than 503
OPENID_NOT_CONFIGURED. Wraps the call in try/catch so the documented
503 response is what callers actually receive.

Adds 4 tests covering the new lookup binding behavior.

* fix(admin-panel): re-check ACCESS_ADMIN on /api/admin/oauth/refresh

The IdP refresh token can outlive a capability/role change, so the
initial requireAdminAccess on the OAuth callback isn't sufficient.
Inject canAccessAdmin via the existing capability model
(hasCapability with SystemCapabilities.ACCESS_ADMIN, matching
requireAdminAccess so custom roles and user grants are honored)
and gate token minting on it. Capability backend errors are
warn-and-denied to keep the bearer-mint path fail-closed.

* fix(admin-panel): scope /api/admin/oauth/refresh to the request tenant

The same (openidId, openidIssuer) pair is allowed across tenants by
the user schema's unique index. The refresh helper was wrapping both
the direct getUserById and the fallback findUsers in runAsSystem,
bypassing tenant isolation, so an IdP identity that exists in two
tenants could resolve to the wrong tenant's user and mint a JWT
bound to that tenant.

Drop the runAsSystem wrappers, add a trusted tenantId option to
applyAdminRefresh, AND it into the fallback findUsers filter, and
assert it against the direct getUserById result. Mount
preAuthTenantMiddleware on the refresh route so the deployment's
X-Tenant-Id header drives the trusted tenant via ALS. Single-tenant
deploys (no header) keep the existing openidId-only behaviour.

Adds TENANT_MISMATCH (401) and a regression covering duplicate
(sub, iss) across tenants plus the direct-userId tenant assertion.

* fix(admin-panel): gate /api/admin/oauth/refresh on OPENID_REUSE_TOKENS

The OSS refreshController only refreshes OpenID tokensets when
OPENID_REUSE_TOKENS is enabled. The body-based admin variant was
unconditionally calling refreshTokenGrant, which made the flag
ineffective for the admin OAuth flow and let admin sessions keep
renewing in deployments that explicitly turned token reuse off.

Add the same isEnabled(process.env.OPENID_REUSE_TOKENS) check up
front and return 403 TOKEN_REUSE_DISABLED so the admin panel BFF
can surface the configuration mismatch instead of silently churning
through retries.
2026-05-08 17:23:02 -04:00
Danny Avila
22890771cf
🧭 fix: Preserve Resend Files for Subagents (#13030) 2026-05-08 17:21:52 -04:00
Danny Avila
a107520109
📦 chore: Bump @librechat/agents to v3.1.81 & npm audit fix (#13027)
* 📦 chore: Bump `@librechat/agents` to v3.1.81

* chore: npm audit fix
2026-05-08 16:20:03 -04:00
Danny Avila
3d5e5348a4
🧵 fix: Include Code Outputs in Thread File Lookup (#13023)
Code-execution outputs land on `messages.attachments` (set by
`processCodeOutput`), while user uploads land on `messages.files`.
The threadFileIds switch (#13004) walked only `files`, so on a
single linear thread:

  Turn 1: assistant produces sample.xlsx → attachment with codeEnvRef
  Turn 2: user says "add 2 rows"
          → primeCodeFiles: file_ids=0 resourceFiles=0
          → /exec sent files=[]
          → sandbox: FileNotFoundError: 'sample.xlsx'

The `getThreadData` walk found zero file_ids because the assistant's
codeEnvRef was on `attachments`, not `files`. Compounded by the
DB select string `'messageId parentMessageId files'` which didn't
pull `attachments` into memory in the first place — so even fixing
the walk in isolation wouldn't have surfaced them.

Both layers fixed:
  - `ThreadMessage` type adds `attachments?: Array<{ file_id?: string }>`
  - `getThreadData` walks both arrays, dedups via the same Set
  - `initialize.ts` selects `'messageId parentMessageId files attachments'`

## Test plan

`packages/api/src/utils/message.spec.ts` (+6 cases):
- collects file_ids from `attachments`
- walks both `files` and `attachments` on the same message
- regression: linear thread with code-output attachments across
  user→assistant→user→assistant produces the right file_ids
- dedupes shared ids that appear in both arrays
- skips attachments without file_id (mirrors `files` behavior)
- empty `attachments` array

`packages/api/src/agents/__tests__/initialize.test.ts` (+1 case):
- locks the DB select string includes `attachments` alongside
  `files` / `messageId` / `parentMessageId`

- [x] `npx jest src/utils/message.spec.ts` — 39/39 pass
- [x] `npx jest src/agents/__tests__/initialize.test.ts` — 33/33 pass
- [x] lint clean on all four touched files
2026-05-08 12:29:46 -04:00
Marco Beretta
26a6312917
🖼️ refactor: Tool Image Outputs outside of Tool Group Auto-Collapses (#12949)
* refactor(attachments): add variant prop to AttachmentGroup

* feat(tool-call): add hideImageAttachments prop to ToolCall

* fix(tool-call): keep MCP image outputs visible when tool group auto-collapses

* test(tool-call): verify MCP images hoist out of collapsed tool group

* fix(tool-call): hoist all grouped attachments and prevent ExecuteCode double-render

- rename hideImageAttachments -> hideAttachments and hide every attachment
  in the inner tool when a group auto-collapses, then hoist them via
  ToolCallGroup with default variant 'all' so non-image attachments survive
  the collapse alongside images
- thread hideAttachments to ExecuteCode so it skips its inline AttachmentGroup
  when grouped, preventing double-render when the group is expanded
- memoize sequentialParts and groupedParts in ContentParts (with
  groupAttachments rolled into each tool-group entry) so we don't re-flatMap
  on every render

* test(tool-call): cover hideAttachments contract and grouping integration

- ToolCall: assert AttachmentGroup is skipped when hideAttachments=true and
  rendered when explicitly false, locking the prop's contract
- ToolCallGroup: update variant assertion to 'all' (now hoists images and
  files together) and add a non-image-only hoist case
- ContentParts.integration: new test exercising the full
  ContentParts -> Part -> ToolCall -> AttachmentGroup chain with realistic
  MCP-shaped data (groups 2+ contiguous tool calls and hoists, single calls
  render inline, mixed image+file hoists, empty attachments are a no-op)

* fix(tool-call): extend hideAttachments to bash/read_file/skill/subagent

When the post-rebase dev branch added BashCall, ReadFileCall, SkillCall,
and SubagentCall as dedicated tool renderers, each rendered its own
inline AttachmentGroup. Once the parent tool group hoists every
attachment, those inline groups would double-render, so they now honor
the same hideAttachments contract as ToolCall and ExecuteCode.

Also seed the new ToolCallGroup mocks (Users icon, getToolDisplayLabel)
so the existing hoist test suite keeps passing on dev.

* fix(image-gen): suppress inline image when attachments are hoisted

OpenAIImageGen renders the generated image directly via <Image>. When
its tool_call lands inside a grouped tool call, the parent now hoists
those attachments into ToolCallGroup's AttachmentGroup, and the inline
<Image> would render the same file a second time. Thread hideAttachments
through Part -> ImageGen (agent-style branch) so the agent-style image
slot stays out of the way once the parent has hoisted.

* refactor(tool-call): drop dead variant prop and flatten render-part hooks

- AttachmentGroup's variant prop ('images' / 'non-images') had no callers
  after the final hoisting design landed, so remove the prop and the
  filtering branches; everything passes the default 'all' behavior.
- Replace the makeRenderPart factory + dual useMemo with two plain
  useCallbacks (renderPart, renderGroupedPart) sharing the same dep set.
- Tighten test mocks: drop 'any' in the new integration test, hoist the
  MCP delimiter constant above its consumer, and remove the now-stale
  data-variant attribute assertion.

* refactor(tool-call): extract getToolCallId helper and tidy imports

- Pull the (part?.[TOOL_CALL] as Agents.ToolCall)?.id chain into a single
  getToolCallId helper in ContentParts so the three call sites stop
  repeating the cast verbatim.
- Re-sort ToolCallGroup local imports longest-to-shortest per the project
  convention.
- Add a Users mock to the integration test's lucide-react stub so future
  subagent-group tests don't trip over an undefined glyph.

* refactor(tool-call): unnest ternaries in subagent and group labels
2026-05-08 12:29:45 -04:00
Danny Avila
a43bc45b73
🧭 fix: Preserve File Search Upload Target (#13019) 2026-05-08 12:29:45 -04:00
Danny Avila
119ac9c944
📦 chore: bump @librechat/agents to v3.1.80 (#13021) 2026-05-08 12:29:44 -04:00
Danny Avila
eb20d8805d
🐛 refactor: anchor code-generated file lookup on threadFileIds for branched conversations (#13004)
* 🐛 fix: anchor getCodeGeneratedFiles on threadFileIds, not threadMessageIds

In a branched conversation (regenerations producing the same code-output
filename), `getCodeGeneratedFiles` would silently exclude files whose
File-record `messageId` lived on a sibling branch. The user-visible
symptom: "the previous file isn't persisted" — the LLM tries
`load_workbook("output.xlsx")` on turn 2 and gets `FileNotFoundError`
because LC sent `_injected_files: []` to codeapi instead of priming
the prior turn's output.

`claimCodeFile` is keyed by `(filename, conversationId, context)` —
not by messageId. When sibling A first creates `output.csv`, the File
record persists with `messageId = A`. When sibling N (a regeneration
of A's parent) recreates `output.csv`, the claim finds A's record and
`processCodeOutput` deliberately preserves `messageId = A` to keep
file→original-creator provenance intact (correct behavior for the
linear case where the original creator is in-thread).

Turn N+1's `parentMessageId = N`. `getThreadData` walks back from N:
the thread is `[N, root]` — sibling A is NOT in it. The pre-fix query
filtered by `messageId IN [N, root]`, so the file was excluded.

`getCodeGeneratedFiles` already lives next to `getUserCodeFiles`,
which has always filtered by `file_id IN threadFileIds` (the file_ids
referenced by `messages.files[]` arrays during the thread walk). The
asymmetry — user-uploaded files anchored on the message's reference,
code-generated files anchored on the File's own creator — was the
bug. Anchoring both functions on `threadFileIds` reaches the right
files regardless of which sibling first generated them.

`File.messageId` stays informational ("who first generated this") for
provenance and `processCodeOutput`'s "preserve original messageId on
update" logic stays as-is — only the lookup key for thread-scoped
fetches changes.

- `packages/data-schemas/src/methods/file.ts`: signature + filter
  change. JSDoc spells out the branched-conversation rationale.
- `packages/api/src/agents/initialize.ts`: pass `threadFileIds` instead
  of `threadMessageIds`. The local `threadMessageIds` declaration is
  removed since the only consumer is gone.
- `packages/data-schemas/src/methods/file.spec.ts`: 5 new cases:
  - basic happy-path (file referenced by current thread)
  - **the regression**: file's creator messageId is on a sibling
    branch but file_id is in threadFileIds → finds it
  - empty/missing threadFileIds returns []
  - cross-conversation isolation
  - non-execute_code context filter still applies (a chat attachment
    won't be returned even if its file_id is in threadFileIds —
    that's `getUserCodeFiles`'s job)

Applies cleanly on top of dev. When LC #12960 (the typed CodeEnvRef
cutover) lands, the only conflict is the legacy `metadata.fileIdentifier`
metadata key flipping to `metadata.codeEnvRef` — same line, trivial
resolve.

- [x] `cd packages/data-schemas && npx jest src/methods/file.spec` —
  42/42 pass (including the 5 new regression cases)
- [x] `cd packages/api && npx jest src/agents` — 722/722 pass
  (modulo 2 pre-existing summarization e2e failures unrelated)
- [x] `cd api && npx jest server/services/Files server/controllers/agents` —
  432/432 pass
- [x] `npx tsc --noEmit -p packages/api/tsconfig.json` — clean
- [ ] Manual: branched conversation reproducer — generate a file in
  turn 1, regenerate the parent (sibling), then in turn N+1 ask the
  agent to read the file. Pre-fix: `FileNotFoundError`. Post-fix:
  the file is primed and load_workbook succeeds.

* 🧪 test: lock initialize.ts → getCodeGeneratedFiles call shape

Integration-level regression test asserting initializeAgent passes
`threadFileIds` (not `threadMessageIds`) to getCodeGeneratedFiles
in branched-conversation scenarios. Locks in the API shape from the
previous commit, sitting one layer above the data-schemas unit test —
so a future refactor to the priming chain can't silently revert to
the messageId-based filter without surfacing a test failure here.

Two cases:
- The full call shape: agent.tools=['execute_code'], resendFiles=true,
  threadData mock returns distinct messageIds and fileIds. Asserts the
  call uses fileIds, and that getUserCodeFiles uses the same array
  (the symmetric design that closes the sibling-branch hole).
- Empty threadFileIds: getCodeGeneratedFiles is still called with []
  (its own internal early-return handles the empty case); getUserCodeFiles
  is gated at the call site and stays unscheduled.
2026-05-08 12:29:44 -04:00
Danny Avila
e7dbae32e5
🚦 fix: Preserve URL Auto-Submit Startup Config (#13017)
* fix: Preserve URL auto-submit startup config

* test: Cover URL auto-submit interface defaults
2026-05-08 12:29:44 -04:00
Danny Avila
0fe203aaca
🧠 fix: charge Gemini reasoning tokens in agent usage accounting (#13014)
* 🧠 fix: charge Gemini reasoning tokens in agent usage accounting

Resolves #13006.

`usage.ts` previously billed `usage.output_tokens` directly. For Vertex
AI Gemini thinking models, `@langchain/google-common`'s streaming path
emits `output_tokens = candidatesTokenCount` only, dropping
`thoughtsTokenCount`. Reasoning was billed at zero and the
`total_tokens === input_tokens + output_tokens` invariant was broken.

The fix lives in agents (danny-avila/agents#157) — but this is also a
defense-in-depth backstop in case agents misses a path or another
provider exhibits the same shape. `resolveCompletionTokens(usage)` adds
`output_token_details.reasoning` back when (and only when) the gap is
present (`total - input > output`), so providers that already include
reasoning in `output_tokens` (OpenAI o-series, Anthropic, the
Google-API wrapper) are no-ops — no double-counting.

- `SplitUsage` gains a `completion` field; all four billing call sites
  in `processUsageGroup` use it instead of `usage.output_tokens`.
- `total_output_tokens` in the result also reflects the corrected
  count.
- `UsageMetadata` interface in `IJobStore.ts` adds the
  `output_token_details` field for type safety.
- 4 new tests in `usage.spec.ts` cover: Vertex undercount fix, OpenAI
  no-double-count, structured spend path with cache + reasoning, no-op
  when no details present.

* 🩹 fix: simplify reasoning correction to invariant-based gap check

Initial fix gated the correction on `output_token_details.reasoning > 0`,
which doesn't help in the live failure case: when google-common's stream
emits the buggy fallback usage_metadata, output_token_details is empty
({}) and the gate exits early.

Live debugging showed the reliable signal is the documented invariant
itself: `total_tokens === input_tokens + output_tokens`. When buggy
streams undercount output, total exceeds input + output by exactly the
unbilled reasoning. Use `total - input` as the corrected output.

This is provider-agnostic and stays a no-op for compliant providers
(OpenAI/Anthropic/Google-via-CustomChatGoogleGenerativeAI), where the
gap is zero.

Live verified end-to-end against gemini-3-flash-preview:
- With agents fix in place:  output_tokens=437 → billed 437  (no-op)
- Backstop only (no agents fix, buggy input): raw 135, billed 297
  (= total 309 - input 12, matches actual API charge)

Updated tests to cover both scenarios.
2026-05-08 12:29:43 -04:00