Commit graph

4909 commits

Author SHA1 Message Date
Danny Avila
e8a943c7e8
📦 chore: bump @librechat/agents to v3.3.8 (#14525)
Activates activity-label continuity end to end. The host side landed with
the activity-groups feature (#14391) — the per-run accumulator that reads
committed headers at request-build time, the `previousLabels` payload
field, resume seeding, and the bridge passthrough — but the SDK had no
field to receive them, so the traced generation path ignored the context
and only the direct fallback rendered it. v3.3.8 carries
danny-avila/agents#356, which adds `previousLabels` to
`RunActivityLabelOptions` and renders it as the label prompt's first
section (capped at 3, each entry whitespace-collapsed and clipped at 200
chars so one malformed header cannot forge prompt sections or inflate
every later request in the run).

Effect: consecutive same-activity batches now extend the run's story
instead of restating a line already on screen, and setup batches stop
being labeled with conclusions their tools had not yet established.

Also included between v3.3.7 and v3.3.8: danny-avila/agents#354, which
anchors summary coverage to a source message id.

Lockfile carries no transitive churn — 3.3.8's dependency tree is
identical to 3.3.7's.

Verified against the installed package: 73 activityLabels tests and 218
api agents-controller tests pass, `tsc --noEmit` clean on packages/api,
and the published build renders the capped, sanitized header section
(oversized labels clipped, embedded newlines flattened to inert text).
2026-07-30 08:23:09 -04:00
Dustin Healy
af795be0c2
🪢 feat: Langfuse Fanout Connection Setting (#14108)
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: encrypt tenant Langfuse secret in admin config

Add generic per-field secret encryption to the admin config layer: registered
secret paths (langfuse.secretKey) are encrypted with encryptV3 on write and a
non-secret fingerprint companion is stored. Admin config reads (base + per
principal) redact registered secrets so they are never returned; the fingerprint
is kept so the UI can show which key is configured.

The Langfuse fanout read path decrypts the tenant secret before export. Adds
secretKeyFingerprint to langfuseConfigSchema and tests for the encrypt/redact
policy.

* fix(api): secure admin config secret handling

* fix(api): preserve encrypted langfuse config secrets

* fix(api): couple config secret fingerprint deletion

* fix(api): read langfuse fanout collector url from env

* fix(api): display langfuse secret key hint

* fix(api): remove langfuse secret fingerprint breadcrumbs

* fix(api): use langfuse destination keys for tenant config

* fix(api): remove langfuse config compatibility fallbacks

* refactor(api): simplify langfuse secret helpers

* refactor(api): simplify langfuse config secret handling

* feat: in-app Langfuse connection settings panel

Add a discoverable, admin-gated Langfuse connection panel inside LibreChat
Settings (Dify-style): enable toggle, host, public key, masked write-only secret,
configured-key fingerprint, and a test-connection action. Backed by a dedicated
/api/admin/langfuse/connection endpoint that encrypts the secret at rest, returns
metadata plus fingerprint on read, and validates credentials. Builds on the
per-field encryption and fanout decrypt from the langfuse-config-encryption branch.

* refactor: align Langfuse secret field to CustomUserVars pattern

Use the established SecretInput plus Set/Unset state pill (com_ui_set/com_ui_unset)
from the MCP CustomUserVars UI for the saved-secret state, instead of a bespoke
masked input.

* fix: drop em dash from saved-secret placeholder

* feat: show loading state on Langfuse test connection button

* feat: gate in-app Langfuse settings on fanout config and admin role

* test: align Langfuse connection spec with SecretInput refactor

* feat(langfuse): refine tenant connection controls

* fix(admin): refine Langfuse connection verification

* fix(langfuse): refine tenant connection settings

* fix(langfuse): simplify export enablement controls

* fix(langfuse): validate tenant export configuration

* fix(langfuse): align startup fanout gate

* fix(admin): time out Langfuse verification

* fix(ui): rename Langfuse connection setting

* fix(admin): enforce Langfuse config capability

* feat(langfuse): require explicit tenant export activation

* feat(langfuse): support single-tenant connection settings

* fix(i18n): remove obsolete integrations label

* fix(langfuse): authenticate ingestion verification

* fix(langfuse): validate public key independently

* fix(langfuse): localize connection errors

* perf(config): skip Langfuse checks for non-admins

* fix(langfuse): preserve trace sampling for feedback

* test(langfuse): fix feedback sampling fixture

* fix(langfuse): align secret preview field

* fix(langfuse): harden connection settings state

* fix(langfuse): preserve trace destination state

* fix(langfuse): enforce tenant-wide routing invariants

* fix(langfuse): preserve verified connection invariants

* fix(langfuse): preserve stable project identity

* fix(langfuse): warm project identity asynchronously

---------

Co-authored-by: Ravi Kumar L <ravi.lazar@clickhouse.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2026-07-29 18:33:10 -04:00
Danny Avila
91adcf3f2c
🪶 fix: Yield Soft Default Model Spec to Agent Picks in Picker-Only Deployments (#14515)
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: Yield Soft Default Model Spec to Agent Picks in Picker-Only Deployments

The `hasEphemeralModelOptions` gate makes the soft default canonical whenever
the selector offers no ephemeral endpoint → model options, so lingering
endpoint/model residue never strands a new chat on an unselectable endpoint.
That gate swept in agent and assistant selections too: under an agents-only
allow-list (`addedEndpoints: [agents]`), every New Chat re-armed the soft spec
and discarded the agent the user had just selected, with no way to make the
choice stick.

An agent pick is the one real selection a picker-only deployment offers, so it
now yields like any other selection, while endpoint/model residue keeps falling
to the soft default.

- Add `hasSelectableEntitySelection`: the stored setup yields when it names a
  non-ephemeral agent_id (or an assistant_id) on an endpoint the allow-list and
  endpoints config still expose. Ephemeral ids, and picks whose endpoint has
  since left the allow-list, stay residue so a stale entity cannot strand a new
  chat.
- Invert the three unit cases that asserted the soft default outranking a stored
  agent under an agents-only allow-list; add coverage for assistants, prioritized
  configs, ephemeral agent ids, endpoint/model residue, an endpoints config
  without agents, and the pre-load allow-list path (35 cases, was 29).
- Add an e2e regression test: under an intercepted agents-only allow-list, a
  selected agent survives New Chat and a cold load, while a cleared instance
  still lands on the soft default.

* 🧹 chore: Type the Intercepted Startup Config in the Soft Default E2E

The agents-only allow-list interception cast the `/api/config` response to
`{ modelSpecs?: Record<string, unknown> }`, discarding the startup-config schema
at the exact point the test rewrites an API response — so a future config shape
change would go unchecked here. Reuse `TStartupConfig` instead, and only rewrite
`modelSpecs` when the response actually carries it rather than fabricating it.
2026-07-29 15:45:23 -04:00
Danny Avila
cc813f430e
🎯 feat: Tool Intent Label Capability (tool_intents) (#14499)
* 🎯 feat: Tool Intent Label Capability (tool_intents)

Adds the fourth member of the per-tool capability family (defer_loading,
allowed_callers, run_in_background): an admin capability
AgentCapabilities.tool_intents plus a per-tool
tool_options[name].describe_intent flag. Opted-in tools get an optional
intent string injected as the FIRST property of their schema — one
model-authored sentence per call, streamed to the client as the call's
live status label (args already reach the client verbatim, so no new
event plumbing). Native host tools (web_search, create_file/edit_file,
set_memory/delete_memory, ask_user_question) default on while the
capability is enabled; explicit false opts out. SDK-native intent
schemas (@librechat/agents coding suite) are recognized and left alone.

- packages/api/src/agents/intent.ts: structural sibling of
  background.ts — first-key non-mutating injection with registry
  parity (covers deferred/tool_search discovery), eligibility and
  PTC-only skips, arg read/strip helpers, self-spawn strip for defs and
  registry, ephemeral/model-spec synthesis with a tool_options merge so
  the background and intent toggles compose.
- handlers.ts: intent runs BEFORE background injection so the label
  stays the first streamed key when a tool carries both (pinned by
  test); the arg is stripped before invocation unless the tool's own
  schema declares it, on both the foreground and background-dispatch
  paths; PTC target schemas are sanitized like background's.
- Capability plumbing through all four routes (endpoint initialize,
  openai + responses controllers, the exported OpenAI-compatible
  service) plus handoff discovery and added-convo agents, and the
  intentToolNames execution channel via configurable.
- describe_intent on toolOptionsSchema (all three written-out Zod
  annotations), ToolOptions, TEphemeralAgent, TModelSpec (+ zod), and
  data-schemas doc comments (tool_options is Mixed — no migration).
- intent.spec.ts: 28 tests cloned from background.spec.ts structure,
  including the intent+background key-order composition.

* 🧯 fix: Codex Review — Opt-Out Strips SDK-Native Intent, Skip mcp_all Placeholders

- An explicit describe_intent: false now REMOVES an SDK-native intent
  property from the definition and registry entry, so the per-tool
  opt-out actually disables the arg's token cost for tools like
  web_search that carry the schema natively (SDK bodies tolerate its
  absence). Previously the early return left the property in place.
- synthesizeIntentToolOptions skips lazily-expanded mcp_all
  placeholders instead of recording options under names that
  applyIntentLabels' exact-name matching can never match, and documents
  the limitation (parity with synthesizeBackgroundToolOptions).

The P1 about the client not rendering the label is the documented
slicing: the UI streaming-label PR follows once #14391's ToolCallGroup
changes merge — args already reach the client, so that slice is purely
rendering.

* 🧯 fix: Codex Re-Review — Label Marker Guard, Capability Kill Switch, Late Defs, Service Threading

- removeIntentParam is now marker-guarded (the label contract's opening
  instruction discriminates it), so an MCP/action tool's own business
  `intent` parameter is never stripped by an opt-out or the disabled
  path — previously an explicit false could remove a real, possibly
  required argument.
- New sanitizeIntentLabels pass runs AFTER every registration step
  (the skill catalog appends its SDK definition post-injection): with
  tool_intents disabled it strips SDK-native intent labels from all
  definitions and registry entries, making the capability a real kill
  switch over their token cost; with it enabled it enforces explicit
  per-tool opt-outs on late-registered definitions.
- ask_user_question removed from the native default-on set: its graph
  tool is rebuilt in run.ts from its own Zod schema (also the HITL
  card's wire shape), so definition-level injection never reached the
  model. Its intent support lands with the HITL slice, which threads
  the label into the interrupt payload deliberately.
- The exported OpenAI-compatible service now threads intentToolNames
  into the run configurable, so the executor's PTC path can strip
  host-injected intent schemas on that route like the in-repo
  controllers do.

* 🧯 fix: Codex Round 2 — Post-Skill Injection, PTC Native Strip, Service Boundary, Honest Docs

- Intent injection now runs LAST in initializeAgent, after the skill
  catalog — which both appends its own definition and REPLACES upgraded
  ones (skill-aware read_file), clobbering an earlier injection while
  intentToolNames still listed the tool. Injection PREPENDS while
  background APPENDS, so intent stays the first schema property under
  the new ordering (pinned by a reverse-order composition test).
- The PTC target-schema strip is now marker-guarded strip-ALL: SDK-
  native intent labels (which are deliberately never in intentToolNames)
  are removed from sandbox-advertised schemas alongside host-injected
  ones; business intent params survive.
- toolIntentsAvailable on the exported service documents the loader
  boundary: a custom LoadToolsFn returning only structured instances
  bypasses definition/registry injection and sanitize by construction.
- librechat.example.yaml describes tool_intents as backend groundwork
  with UI rendering in an upcoming release rather than promising a live
  label today.

* 📦 chore: bump `@librechat/agents` to v3.3.6

Brings in the SDK half of tool intent labels (danny-avila/agents#347,
#349): intent-first schemas on the coding suite across all three
engines, plus web_search / subagent / skill / tool_search, and the
outcome / outcome_patch result channel.

Activates three host paths that were inert while no SDK tool shipped an
`intent` property — verified against the real 3.3.6 schemas:
- capability OFF now strips SDK-native labels (a real admin kill switch)
- explicit `describe_intent: false` removes them per tool
- host injection stays idempotent against an SDK schema, keeping
  `intent` first and never double-injecting

* 🔬 test: Real-Provider Verification for Tool Intent Labels

Adds the live check the unit tests structurally cannot perform: whether a
real model actually authors the injected arg, places it FIRST, and gives
sibling calls to one tool distinct labels. Reuses the existing
real-provider harness (in-memory Mongo, seeded user, credential
neutralizer) and the existing stdio MCP fixture as a genuine tool, so no
external service is involved.

- e2e/config/librechat.real.yaml: adds the e2e-memory MCP server and the
  tool_intents capability, giving the real model something to call. The
  sibling spec asserts only relative token growth, so the extra schemas
  do not perturb it.
- e2e/playwright.config.real.ts: optional Langfuse passthrough. The
  LANGFUSE_* keys match the credential-neutralizer pattern and were being
  blanked before the server booted; they are preserved explicitly, read
  from the invoking environment only, and never written to the generated
  config.
- e2e/specs/real/tool-intents.spec.ts: two facts stored in one turn, both
  through the same tool, asserting intent is the first key of each call
  and that the two labels differ. Args are read from persistence rather
  than the DOM deliberately — no UI renders the label yet, and
  persistence is what a reloaded conversation and the trace both read.

First run against claude-haiku-4-5 produced 'Recording the location of
the OAuth callback router' and 'Recording the location of the MCP
connection pool configuration' — distinct, first-position, no tool name.

Also updates tool-intent-spec.md: records the 3.3.7 removal of the tense
verb map with the evidence that motivated it, the trimmed description and
the marker's role as an API, and a new mandatory requirement that
client-side label rendering be gated on a server-sent signal rather than
the presence of an intent key (a tool's own business 'intent' parameter
would otherwise render as a status label).

* 📦 chore: bump `@librechat/agents` to v3.3.7 and dedupe the intent contract

Picks up danny-avila/agents#353: the tense verb map is gone (a bare
intent now displays unchanged, with completion carried by UI state), the
model-facing description is trimmed 502 → 289 chars, and both the marker
and the description are exported.

Stops redeclaring the SDK contract here:
- INTENT_LABEL_MARKER is imported instead of duplicated as a string
  literal. Every removal path in this module keys on it, and a local copy
  that drifted from the SDK's would make them all stop recognizing
  SDK-native labels — failing OPEN, with labels left in schemas and
  per-tool opt-outs silently inert.
- INTENT_DESCRIPTION is imported too, so host-injected tools and
  SDK-native tools present the model with one identical instruction.
  Keeping the old local copy would also have meant host-injected tools
  still paying ~126 tokens per schema while SDK tools paid ~72.

Verified live against real Anthropic after the trim: two sibling calls to
one MCP tool produced 'Storing the OAuth callback router file location'
and 'Storing the MCP connection pool configuration file location' —
first-position and distinct, so the shorter description holds compliance.
2026-07-29 15:40:52 -04:00
Danny Avila
d70cab48fd
🎯 fix: Keep Run Steps and Labels in One Index Space After a Resume Sync (#14516)
Follow-up to #14391, which deliberately left this shared math untouched.

An edited resubmission offsets incoming indices past the prefix the client
retained, because the server indexes only NEW content. A resume sync
invalidates that arrangement twice, and run steps honored neither:

- It REPLACES `initialResponse.content` with the server's completion-local
  snapshot, so the live array stops measuring the retained prefix. Run
  steps derived their offset from that array, so a reconnect that produced
  an empty snapshot silently dropped the offset to zero and wrote over
  retained content.
- When it also replaces the RENDERED content, the prefix is gone entirely
  and server indices are already absolute. Run steps kept adding the
  snapshot's own length on top, writing past the end and leaving holes.

Activity labels already honored both facts (`editPrefixLength` +
`editPrefixClearedRef`), so a batch's tool cards and its header could
resolve in different index spaces: a label overwriting an unrelated part,
or a fill missing its own reservation and leaving the placeholder pending
forever.

Run steps now read the same two inputs. `useStepHandler` takes the
CAPTURED `editPrefixLength` rather than measuring the live array, gated on
a new `editPrefixCleared` flag that the resumable transport — which owns
the sync boundary — stamps onto dispatched submissions. The non-resumable
transport never sets it, so the plain edit path is unchanged.
`calculateContentIndex` now takes the offset directly instead of the
prefix array, so its ±1 trailing-text adjustment cannot diverge from the
offset every other path applies.

Tests (useStepHandler.spec): unedited applies no offset; a plain edit
still offsets; the captured length wins when sync replaced the live array;
a cleared prefix stops offsetting for both run steps and message deltas,
staying at absolute indices. Verified against the pre-fix code — the three
states this PR repairs fail there and pass here.
2026-07-29 15:39:10 -04:00
Danny Avila
becfc5a373
🗝️ feat: Custom Endpoint API Key Encryption via Array Secret Registry (#14510)
Extend the admin-config secret registry (#14509) with array-item secrets,
registering endpoints.custom[*].apiKey: encryptV3 at rest with apiKeyPreview
companions, redaction on admin reads (plaintext-legacy included, with
verbatim-name omit-to-keep preservation, duplicate-identity skip, and
legacy-plaintext self-healing), passthrough for user_provided/${ENV} refs,
strict-payload runtime decryption at getCustomEndpointConfig, the custom
model fetch, and the provider fallback, and rejection of named/indexed/
positional writes beneath the protected array path.
2026-07-29 15:37:46 -04:00
Danny Avila
7b6900d556
🏷️ feat: Activity Groups With Fast-Model Headers (#14391)
* ✨ feat: Activity Groups with Fast-Model Labels

Groups each contiguous block of reasoning + tool calls into a collapsible
unit headed by a fast-model label (claude.ai-style hierarchy), off the
critical path: a PostToolBatch hook claims a live content slot at the
batch boundary (steering index-offset pattern), renders a deterministic
counts phrase instantly, and swaps in the generated label ~1s later while
the next model call streams. Labels are UI-only — stripped before the SDK
formatter and skipped in the legacy formatter — and reach live clients
via a dedicated on_activity_label SSE event (live/replay/pending paths).

Grouping preserves legacy rendering byte-for-byte when no label part is
present. Generation bridges to Run.generateActivityLabel() when the SDK
ships it (session-grouped Langfuse tracing); falls back to a direct call
today. Env-gated: ACTIVITY_LABELS_POC=true, ACTIVITY_LABEL_MODEL.

* 🧷 fix: Address Codex and Copilot Review Findings for Activity Labels

- Settle in-flight label fills (bounded 3s) before finalization on both
  the main and resume paths, so a label resolving during the final batch
  still reaches the durable log and saved message.
- Overlay on_activity_label chunks in RedisJobStore content
  reconstruction (splice path last-wins per index; replay path
  chronological overwrite), matching steer handling.
- Wire activity labels into the HITL resume createRun so post-resume
  batches keep claiming slots.
- Guard against out-of-order publishes: fill() awaits the claim emit
  before emitting the resolved label, and the client applier ignores a
  stale pending placeholder once a resolved label is present.
- Stamp the batch's groupId onto label parts so parallel-column runs
  place them inside their group instead of filtering them out.
- Localize the counts fallback phrase (10 keys, singular/plural) through
  useLocalize across chat rendering and exports.
- Type the hook with Providers/ClientOptions instead of stringly types;
  drop the unknown cast in the spec; add a dedicated rAF retry ref for
  label events with effect cleanup.

* 🛡️ fix: Address Independent Review — Abort, Usage, Lane Context, Redis Test

- Propagate the run abort signal into label generation (both wiring call
  sites; runtime combines host + dispatch signals with the timeout) so a
  user abort cancels in-flight label calls instead of paying to timeout.
- Record label-call usage like titles: the SDK bridge aggregates via
  chainOptions callbacks, the fallback path via a per-generation callback
  factory; both feed recordCollectedUsage under context 'activity-label'.
- Scope block-context capture: reasoning collection stops at the previous
  block's label part and filters by executingAgentId, so consecutive or
  parallel batches can no longer bleed another block's thinking into the
  payload; intent text still scans past labels (persists across batches).
- Forward the effective charLimit to the SDK call so host and SDK prompts
  agree (SDK default aligned to 600 in agents#327).
- Add a Redis integration test proving last-write-wins reconstruction of
  on_activity_label chunks per claimed index.
- Rebased onto main: only the two activity commits replay (the nine
  steering commits belonged to the old base branch), zero conflicts,
  steering suites green.

* 📐 refactor: Move Activity-Label Wiring to TypeScript, Address Codex Round 2

- [P1] Slot claiming, lane stamping, emit ordering, context capture, and
  settle tracking now live in packages/api (createActivityLabelWiring +
  captureActivityBlockContext); client.js is a thin closure wrapper.
- Register the activity-label hook BEFORE the steer drain so a steer
  draining at the same batch boundary cannot flush the tool block and
  orphan the label outside its group.
- Resolve request-based header placeholders in resolveActivityLabelLLM
  (titleConvo parity) so metadata-keyed proxies work on label calls.
- Trim labels centrally before filling so whitespace-only output from
  either generation path keeps the deterministic counts fallback.

* 🧭 fix: Codex Round 3 — Capture Order, Shared Strip, Token Estimator, Hide Filter

- Capture block context BEFORE pushing the label part: the scan stops at
  ACTIVITY_LABEL parts, so post-push capture hit the just-inserted label
  and silently collected no reasoning excerpts (regression test added).
- Share stripActivityLabelParts from packages/api and apply it in the
  Responses and OpenAI-compatible controllers, closing the replay leak
  for entry points still running SDKs without the formatter skip.
- Exclude activity_label parts from the fallback response-token estimator
  (UI-only parts must not inflate no-usage provider billing).
- Keep label parts explicitly under hide_sequential_outputs — they
  summarize exactly the outputs that mode hides.

* 🔁 fix: Codex Round 4 — Resume Gap, Delta Flush, Agent-Scoped Intent, Token Counter

- Synthesize on_activity_label events for labels claimed or filled in the
  snapshot→subscribe window (the publish is fire-and-forget, so Redis-mode
  reconnects missed them). Feature-gated so the default path adds no
  content re-read; the client applier already ignores duplicates.
- Flush queued deltas before applying a label part, matching the pending-
  action and steer appliers — without it the handler read a stale message
  cache and syncStepMessage pushed a pre-delta copy back.
- Skip another agent's tail text when resolving intent, so parallel runs
  cannot seed a label prompt with a sibling agent's narration.
- Exclude activity_label parts from countFormattedMessageTokens (the
  agent-path counter), not just the legacy BaseClient one.

* 🏗️ refactor: Codex Round 5 — Extract Label Host Logic, Report Usage, Icon Strip

- Move provider/model resolution, usage-metadata mapping, and the settle
  loop into packages/api (activityLabels/host.ts); client.js keeps only
  thin delegations, per the repo's TypeScript-implementation convention.
- Fold label usage into the response rollup with an 'activity-label' tag
  (subagent precedent) so metadata.usage and the live cost gauge account
  for it; tagged, so it stays out of PRIMARY usage/context pairing.
- Narrow tool metadata once in ToolCallGroup so THINK parts in a labeled
  block no longer render phantom generic icons in the stacked strip.
- Import the activity-label helpers by deep path in GenerationJobManager:
  the package barrel now reaches provider-config/cache modules that
  import back into the stream layer, and the cycle broke suite loading.

Declined: resetting steerOffsetState before HITL resume — resume builds a
FRESH AgentClient via initializeClient (initialize.js:978), so the offset
is already zero; the seed wrapper alone accounts for pre-pause parts.

* 🚦 fix: Codex Round 6 — Stream Label Usage, Close Late Fills

- Emit an on_token_usage chunk for label calls (sink push alone left the
  live session gauge blind); retained in pendingSubagentEmits so job
  cleanup cannot race the persist, tagged 'activity-label' as before.
- Close the label scope when settle times out: the wiring gates fill() on
  isClosed and the client fires a label-scoped AbortController, so a
  straggling generation can neither mutate a saved response nor emit into
  a job whose runtime is gone. The controller also chains to the run
  signal, so a user abort still cancels label work.

* 🩹 fix: Repair CI — Package Typecheck and Module Mocks

Local runs covered the client tsconfig and jest, but never packages/api's
own tsconfig, so nine type errors in the extracted host module shipped.

- Type host.ts against the real contracts: ServerRequest, EndpointDbMethods,
  AppConfig from @librechat/data-schemas, IUser for createSafeUser, and a
  MaybeAzureConfig view for the azure instance-name probe and configuration.
- Widen resolveConfigHeaders' llmConfig to Partial<RunLLMConfig>: it only
  reads the three provider header carriers, so auxiliary generations with a
  bare ClientOptions can resolve headers without assembling a run config.
  Type-only widening; every existing caller still satisfies it.
- Add stripActivityLabelParts to the @librechat/api mock in the OpenAI and
  Responses controller specs — those mocks enumerate exports, so a new
  import read as undefined and threw before the assertions ran.
- Use the real activity-label helpers in the ToolCallGroup spec's ~/utils
  mock; stubbing them out would hide the header logic under test.

* ⚙️ feat: Configure Activity Labels via librechat.yaml, Drop Env Vars

Replaces the ACTIVITY_LABELS_POC / ACTIVITY_LABEL_MODEL env gate with
per-endpoint settings, following the title options convention rather than
a top-level block — each endpoint picks its own cheap label model.

- Add activity, activityModel, activityEndpoint, activityPrompt,
  activityMaxPerRun, and activityCharLimit to the endpoint schema, and to
  the endpoints.all pick list (enumerated, so 'all:' would otherwise drop
  them silently).
- resolveActivityConfig reads them with title-style precedence:
  endpoints.all > named endpoint > custom endpoint config.
- Model precedence is now activityModel > titleModel > the agent's model.
  activityEndpoint runs labels on another endpoint's credentials, with
  titleConvo's fallback-on-unknown-name behavior.
- Thread activityPrompt/MaxPerRun/CharLimit through the wiring into the
  hook and the SDK bridge; they were hardcoded defaults.
- The resume gap-repair gate keyed on the env var; it now keys on the
  snapshot actually containing label parts, so deployments without the
  feature still perform no extra content read.
- Document the fields in librechat.example.yaml; add host.spec.ts
  covering precedence, custom-endpoint fallback, and opt-out.

* 📝 refactor: Rename Enable Flag to activityLabel, Document Schema Inheritance

- Rename the boolean from `activity` to `activityLabel`, matching the
  titleConvo/titleModel shape: a verb-object toggle whose prefix matches
  its modifiers (activityModel, activityPrompt, ...). `activity: true`
  alone read ambiguously — it could mean tracking or logging activity.
- Document the two endpoint-schema inheritance paths, which behave
  oppositely and are ~900 lines apart:
  * `endpoints.all` omits from baseEndpointSchema, so new options are
    inherited automatically — nothing to maintain.
  * `azureEndpointSchema` enumerates via .pick(), so a new option is
    silently unavailable on Azure endpoints until listed there.
  The activity block now carries a pointer to the Azure caveat.

* 🔍 fix: Address Codex Findings on the Config Rework

- Pass the matched custom-endpoint config into the label gate. Custom
  endpoints live in the `endpoints.custom` ARRAY, so without it every
  custom endpoint resolved as disabled — including the example this PR
  added to librechat.example.yaml.
- Give label usage a unique `runId:seq`. Label usage is billed but never
  appended to `collectedUsage`, so its length was static: every label
  event reused the last primary usage's pair and collided with itself,
  and the client dedupes on exactly that.
- Attach `cost` to label usage when `interface.contextCost` is on;
  aggregateEmittedUsage treats coverage as all-or-nothing, so an event
  without it suppressed the whole response's cost.
- Honor `activityPrompt` on the direct fallback path, not just the SDK
  bridge — it previously always used the built-in instruction.
- Seed the per-response label cap from labels already on the response so
  a HITL resume cannot mint a fresh quota after every approval.
- Reconcile label gaps on resume via a durable per-job `activityLabels`
  flag instead of probing the snapshot: the FIRST label of a run can be
  claimed inside the snapshot->subscribe window, which the old signal
  missed. The flag is read from a job record already fetched there, so
  runs without the feature still add no content read.
- Auto-collapse labeled single-tool groups; one-call batches are common
  in agent runs and rendering them expanded defeats the grouping.

* 🎯 fix: Correct Label Usage Seq, Cross-Endpoint Pricing, Close Scopes

- Give label usage a NEGATIVE seq namespace. The previous fix was wrong:
  seq is a position in `collectedUsage` (push, then emit with the new
  length), so sink-length + array-length still lands on a real position —
  primary emits 1, the label computes 2, the next primary also emits 2.
  Labels have no position at all (billed separately, never appended), so
  they now occupy a namespace positional sequences cannot reach. The
  client key is a string used for Set membership, so the sign is inert.
- Price cross-endpoint labels with the LABEL endpoint's token config:
  resolveActivityLabelModel now returns the resolved endpointTokenConfig,
  and both the streamed cost and recordCollectedUsage use it instead of
  the agent endpoint's rates.
- Make close state per-wiring rather than per-client. A HITL resume
  rebuilds the wiring, and resetting a shared flag re-opened closures from
  the pre-pause segment whose provider call ignored the abort; settle now
  closes every retained scope, past generations included.

* 🎯 fix: Make the Activity Header Say Something the Cards Cannot

The header read "ran 1 command" next to a card already labeled "Code" —
it restated the UI beneath it instead of adding to it. Two causes, both
about content rather than timing:

- A deterministic tool-type tally was the primary display and also fed
  the prompt, so the best case was a tally and the worst case was a
  tally dressed as prose. Removed from the metadata, the prompt, the
  part type, and the client.
- The instruction only ever reached the fallback path. The wiring
  passed a prompt only when  was configured, so the
  preferred SDK path silently used the published package default. The
  wiring now always supplies one and the hook forwards it on both
  paths.

The register is rewritten around what the cards cannot show:
past-tense git-commit-subject, leading with the distinctive noun,
outcome over attempt, tool names and counts and arguments explicitly
forbidden. The batch entries are labeled as reference material so the
model stops transcribing them.

Claiming a slot no longer emits. The slot still reserves its index so
streamed parts never collide, but with nothing to say there is nothing
to render: until a description exists the block looks exactly as it
does without the feature.

* 🧹 fix: Drop the Localize Hook Left Unused by the Counts Removal

* ✅ test: Add Activity-Label e2e Coverage with a Recording Label Server

Activity labels are the one model call a mock run does not already fake:
fake-model.js swaps the GRAPH model via overrideTestModel, while
run.generateActivityLabel() calls the endpoint resolved client options
over HTTP. The custom endpoints already point baseURL at 127.0.0.1:8889,
so serving that port exercises the real path with no production seam.

fake-label-server.js answers it in both JSON and SSE form, records each
prompt, and can inject blank/error responses. Recording is what lets the
spec assert the CONTRACT rather than the rendering: that this repo
register and the tool OUTPUTS actually reach the model. That is the bug
class that produced unusable labels before, and rendered text looks
identical whether or not the instruction arrived.

Labels get a dedicated endpoint (Mock Provider E). A labeled block
auto-collapses even at one tool call, which hides the tool cards other
specs assert on -- enabling this on a shared endpoint broke
steering.spec.ts. Provider D is the unlabeled control.

Request-count assertions are scoped to a per-test token: a 5xx label
response is retried by the provider client, and a retry can land after
the next test has reset the server.

* 🩹 fix: Address Review Findings on Activity-Label Indexing and Pricing

Replay index (P1). Reserving the slot only in server memory left no event
for it, so a cross-instance replay rebuilt content as [tool, hole, later],
compacted the hole away, and the fill for the reserved index landed on the
following part and overwrote it. The claim now publishes the empty,
pending part so the index is real for every consumer, and fill publishes
even when generation returned nothing so the client cannot stay pending.

It stays invisible: an empty label still DELIMITS its batch in
groupSequentialToolCalls but is not attached as the header, so grouping
does not re-shuffle when the text lands and the block renders exactly as
it does with the feature off.

Edited-response index (P1). Edit-and-resubmit replays the kept prefix and
the server indexes only new content, so run steps offset by that prefix.
Labels are claimed in the same space and now take the identical shift;
without it a label could land inside the prefix and overwrite it.

Redis flag. deserializeJob never read activityLabels back, so every Redis
reload left it undefined and resume skipped label gap reconciliation.

Executing agent. RunActivityLabelOptions.agentId selects the executing
agent tracing metadata AND its tool-output redaction policy; omitting it
let a handoff be redacted under the default agent configuration.

Label pricing. An undefined endpointTokenConfig is meaningful for a
built-in label endpoint (priced from the shared table), so the nullish
fallback billed those labels at a custom primary rates. Inherit only when
the label runs on the agent own endpoint.

HITL usage sequence. runId is the response message id and the counter was
instance-local, so a resume restarted at -1 and the client runId:seq
deduper discarded the post-approval label usage. Seeded past the labels
already on the response.

Also distinguishes "cannot serve" (undefined) from "no label" (null) in
the SDK bridge, so a missing run falls back to the direct call instead of
filling the slot empty. Version gating already happens at wiring time via
the sdkCapable prototype probe.

* 🩹 fix: Keep Unfilled Activity Labels Invisible and Unmask Endpoint Settings

Follow-up review round. Publishing the reservation on every batch made two
latent rendering paths reachable on every run, and both are fixed here.

Empty labels no longer change grouping. The previous pass still formed a
tool-group for a textless label, which wrapped even a single tool call and
pulled THINK parts inside it — and since a reservation is published the
moment each batch ends, that applied during every generation and
permanently after a blank or failed fill. An empty label now flushes the
legacy way instead: it still delimits its batch, but the block re-splits
exactly as it renders with the feature off.

Parallel lanes no longer show a blank line. Lanes render raw parts, so an
unfilled label had nothing to draw; empty ones are dropped. Making labels
act as collapsible headers inside lanes is still a separate gap.

Edited responses no longer offset on resume. The sync replaces
initialResponse.content with the server's aggregatedContent, which already
contains the kept prefix AND everything generated since — so its length is
not the prefix length, and indices reconciled from that snapshot are
already absolute. Offsetting again pushed the label past its slot onto a
later part. The shift now applies only to a fresh edited submission.

Activity settings resolve field by field. Selecting one config object
whole meant any endpoints.all block — even one carrying nothing but
headers — shadowed the named or custom endpoint and silently disabled
activity labels everywhere. Global still wins per field.

Adds groupToolCalls coverage for the invisible-while-empty contract, which
is the part most likely to regress: it is normal state on every run, not
an edge case.

* 🔒 fix: Scope Detached Label Writes to Their Generation Epoch

Epoch scoping (P1). Label generation is detached and can outlive the
generation that started it. emitChunk only proves that SOME runtime is
current, not that the caller belongs to it, so an aborted generation's
fill(null) -- and its usage event -- could be attributed to whichever
generation replaced it, landing an index from the abandoned response on
top of the new one. Because an empty label renders nothing, that
overwrote content silently. emitChunk now takes an optional jobCreatedAt
and drops the event when the runtime epoch differs, mirroring the
existing setGraph/setContentParts convention, and both label emitters
pass it.

An abort now CLOSES the label scope instead of only cancelling the call:
the rejected generation still runs its catch and calls fill(null), which
would otherwise emit into a stream the next generation may already own.

Edited-response indexing (P1). The previous pass skipped the prefix
offset on resume, which was the wrong half of the problem: a sync
replaces initialResponse.content with the server's aggregatedContent,
which is completion-local, so after a reconnect its length is not the
kept-prefix length and the offset is wrong -- but it is wrong for run
steps in exactly the same way. Tool cards and the label that heads them
must share one index space; a label shifting differently from its tools
lands on another part. The label path now uses the identical expression
as useStepHandler, with no resume special-case. Correcting the
post-resume prefix length belongs in calculateContentIndex, where it
fixes both at once.

titleModel masking. The activity settings were made per-field last pass,
but the titleModel fallback a few lines below still selected an entire
config object, so a partial endpoints.all (for example one carrying only
headers) hid a named endpoint's titleModel and quietly fell the label
back to the main agent model. Both now read through one shared per-field
helper.

Resume reconciliation no longer depends solely on markActivityLabels,
which is best-effort yet had come to gate correctness: a lost flag write
silently dropped a label. The snapshot is consulted as a fallback.

The exported host type for generateLabel now admits undefined, which is
the documented "cannot serve, fall back to the direct call" signal the
hook keys on -- distinct from null, meaning it ran and produced nothing.

* 🧷 fix: Keep Group Identity Stable and Memoize Label Endpoint Resolution

Group remount. Tool-group identity was keyed on the first part in the
block. An activity label absorbs the block's leading THINK part the moment
its text lands, so the key flipped from tool:<id> to fallback:<scope>:<idx>
mid-run, remounting the group and discarding whatever the user had
expanded. The key now scans for the first tool call, which does not move
when the block re-forms.

Label endpoint resolution is memoized per response. It reads provider
config and can hit the database for user keys, yet nothing it depends on
changes between batches of one run — and it ran twice per batch, once for
generation and once for usage accounting. The promise is cached rather
than the value so concurrent batches share a single in-flight resolution,
and a rejection is evicted so one transient credential failure cannot
disable labels for the rest of the response.

* 🎯 fix: Offset Edited Resubmissions by a Prefix Length That Survives Resume

The server indexes only NEW content for an edited resubmission, so the
client offsets incoming indices by the prefix it retained. That prefix was
read as initialResponse.content.length, which is correct only until a
resume: the sync replaces that array with the server's completion-local
snapshot, whose length is unrelated to the prefix. After a reconnect every
offset was therefore wrong -- run steps and activity labels alike -- and
could write over content the edit kept. For a label the symptom is worse
than a bad position: the fill misses its own reservation, so the pending
placeholder is never resolved.

The prefix length is now captured when the submission is built, while
initialResponse.content still IS the retained prefix, and carried on the
submission as editPrefixLength. calculateContentIndex takes that length
instead of deriving it from an array that a resume may have replaced, so
run steps and labels share one index space by construction rather than by
both happening to read the same field.

Note the prefix is the FULL original content with the edited part
substituted in place (useChatFunctions clones latestMessage.content and
mutates one entry) -- it is not a slice, so the length cannot be inferred
from editedContent.index.

Group identity no longer changes when a label fills. Tool-group keys were
derived from the first part in the block; an activity label absorbs the
leading THINK part when its text lands, flipping the key mid-run and
remounting the group, which discarded the user's expansion state. The key
now scans for the first tool call, which does not move.

Label endpoint resolution is memoized per response. It reads provider
config and can hit the database for user keys, yet ran twice per batch --
once to generate, once for usage accounting -- while nothing it depends on
changes within a run. The promise is cached so concurrent batches share one
in-flight resolution, and rejections are evicted so a transient credential
failure cannot disable labels for the rest of the response.

The resume gap passes for steers and activity labels now share a single
lazy content read instead of each issuing its own. The label pass stays
gated on the run flag with a snapshot fallback: reconciling
unconditionally would also close the residual first-label window, but it
would bill a read to every resume of every run, including deployments with
the feature off -- which the steer pass deliberately avoids. That residual
requires a lost flag write, which shares fate with the content writes the
labels live in.

* 💵 fix: Bill Cross-Endpoint Labels at Their Own Rates

recordCollectedUsage never accepted an endpointTokenConfig, so the value
the activity-label caller passed was dropped and the balance transaction
was written at the primary agent's rates. Only the UI cost honored the
label endpoint, so a custom primary pointing activityEndpoint at another
endpoint showed one price and charged another. The parameter is now
accepted, and an explicit config wins outright over per-agent resolution:
that map is keyed by AGENT, so it cannot describe usage that ran on a
different endpoint.

Group identity is stable for id-less tool calls too. The previous pass
anchored the key to the first tool call ID; where a supported tool call
carries no id the fallback still used the block's first part index, which
shifts when a filled label absorbs the leading THINK part. The fallback now
anchors to the first TOOL entry's index, so only a block containing no
tool call at all keys off parts[0].

markActivityLabels is retried rather than fire-and-forget. It gates resume
gap reconciliation and is a SEPARATE write from the durable label append,
so a single lost write silently drops a label the content itself recorded.
The earlier "shared fate with content writes" reasoning was wrong. One
retry at run setup costs nothing and removes the only realistic way the
gate goes stale, without billing a content read to every resume.

* 🧮 fix: Stop Offsetting Once SYNC Drops the Edited Prefix

The edit offset was applied unconditionally, but whether it is correct
depends on which branch SYNC took. SYNC either preserves the content
already loaded for the response -- which still contains the retained
prefix, so the offset is required -- or replaces it with the server's
aggregatedContent, which is completion-local and indexed from zero, after
which any offset writes past the end of a now shorter array.

That is why the two previous attempts each fixed half of it: skipping the
offset on resume was right for the replace branch, applying it
unconditionally was right for the preserve branch, and neither holds on its
own. The offset now tracks the actual state of the rendered content.

For an activity label the replace branch was worse than a bad position:
the fill landed past its own reservation, so the pending placeholder was
never resolved and the block kept its generic header for the rest of the
run.

Applied to run steps as well, not just labels. useStepHandler reads the
prefix from the same submission and had the same unconditional offset, so
after a mid-session resume of an edited response tool cards were misplaced
too. Normalizing at the dispatch boundary keeps both in ONE index space by
construction: a label that shifted differently from the tools it heads
would land on another part.

Note the reload path was already coherent -- useResumeOnLoad rebuilds the
submission without editedContent or editPrefixLength, giving no offset
against server-supplied content -- so only the mid-session SYNC path was
inconsistent.

* 🧾 fix: Keep Label Accounting Out of the Primary Usage Slot

Label usage no longer owns getStreamUsage(). recordCollectedUsage assigned
its result to this.usage unconditionally, so when the primary provider
reported no usage metadata but the label provider did, BaseClient took the
label's output tokens as the assistant response's authoritative count. The
later primary call returns early on an empty collectedUsage and never
replaced it, so the wrong value stood, the text-based fallback was skipped,
and the real generation went unbilled. Secondary usage is still billed but
no longer writes that slot.

Cross-endpoint pricing keys off an explicit discriminator rather than the
presence of a value. A built-in label endpoint prices from the shared
table, so an undefined endpointTokenConfig is its MEANINGFUL value --
reading that as "no override" fell back to the primary's custom rates and
restored the exact mismatch the previous pass set out to fix. The caller
already knows whether the label ran elsewhere and now says so.

markActivityLabels rejects on failure instead of swallowing it. The flag
gates resume gap reconciliation and the caller retries it, but the internal
catch resolved successfully and made that retry unreachable -- so the two
changes cancelled out and a transient write failure still left the flag
absent.

Late label accounting is suppressed with the same gate as the late fill. A
straggler that outlived the settle timeout still ran its finally block, so
it charged the balance and appended to usageEmitSink after the response had
passed its usage flush and metadata snapshot: a cost the user pays but is
never shown.

The cleared-prefix state is scoped to one generation. It was set on a
resume SYNC that replaced the response and then never reset, so a later
edited resubmission in the same mounted hook dispatched run steps and
labels with no offset against content that still held its retained prefix.
Reconnects pass isResume and keep the state; a new generation clears it.

* 🔑 fix: Key Prefix State to the Stream and Honor current_model for Labels

The cleared-prefix reset keyed on isResume, which skips exactly the case it
was added for: a submission whose POST succeeded server-side but lost its
response is retried, comes back resumed: true, and subscribes in resume
mode even though it is a NEW generation. A previous generation's cleared
state then survived into it, and incoming run steps and labels applied no
offset against content that still held its retained prefix. The state is
now keyed to the stream id, which changes with the generation and stays put
across reconnects of one.

activityModel now honors current_model. The options are documented as
title-shaped and the titleModel fallback already excludes the sentinel, but
the higher-precedence activity override passed the literal string through to
getOptions and the provider, so an endpoint following that convention failed
every label instead of using the agent model.

* 🎯 fix: Key Prefix State to the Generation and Resolve the Run Model

The cleared-prefix state was keyed to the stream id, which never changes
within a conversation: request.js sets streamId = conversationId, so once a
reconnect cleared the state every later edited resubmission in that
conversation dispatched run steps and labels with no offset and could
overwrite the prefix it retained. It is now keyed to the response message
id, the only per-generation identity available here -- minted per
submission and carried through a resume unchanged.

That is the third identity tried for this state. isResume missed the
deduplicated-retry path (a lost response returns resumed: true for a new
generation); the stream id is conversation-scoped. The response id is the
boundary that actually matches a generation.

current_model labels now resolve the model the run is really using.
initializeAgent merges the request's endpointOption override into
model_parameters and the run gives it precedence, so preferring the saved
agent.model could send labels to a different, potentially unavailable or
more expensive model than the conversation is on.

* 🆔 fix: Key Prefix State to the Submission and Keep the Origin Title Model

Editing an assistant response reuses that response's messageId as
editedMessageId, and useChatFunctions carries it onto
initialResponse.messageId -- so re-editing the same response produced two
generations with the same key and the cleared-prefix state survived between
them, leaving run steps and labels with no offset against content the edit
retained. Keyed now to clientRequestId, the per-submission uuid, which is
minted fresh per edit attempt and forwarded unchanged on retries.

That is the fourth key this state has had, and each earlier one failed at a
real boundary: isResume missed the deduplicated-retry path, the stream id is
the conversation id, and the response message id is reused across edits of
one response. clientRequestId is the identity that actually means "this
submission".

The titleModel fallback is read from the ORIGINATING endpoint again, matching
how titleConvo captures its config before switching credentials. Reading it
after an activityEndpoint switch meant an OpenAI endpoint configured with
titleModel claude-haiku and activityEndpoint anthropic fell through to the
OpenAI run model and sent that name to Anthropic, failing every label. The
destination endpoint supplies credentials, not the model choice.

* 🧷 fix: Close the Remaining Edit, Epoch, and Scope Gaps for Labels

SYNC clears the edit prefix on the new-row branch too. When a resumed
edited submission cannot match an existing assistant row, that branch
builds the response straight from the server's completion-local
aggregatedContent, so it holds no retained prefix -- but the reset lived
only in the matched branch, leaving later steps and labels adding an
offset to indices that were already absolute.

Label usage is keyed per GENERATION. Editing one assistant response reuses
its responseMessageId while each fresh generation restarts
activityLabelUsageSeq, so a second edit re-emitted the same runId:seq and
the client discarded the newer usage while its balance transaction was
still written. The key now carries jobCreatedAt, the run's own epoch:
stable across reconnects and HITL resumes, distinct between generations.

The scope is revalidated at commit time. Checking once before the await let
a scope that closed mid-flight still charge the balance after finalization,
while the matching fill saw the closed scope and dropped the label --
billed but never surfaced, the exact outcome the guard exists to prevent.

The titleModel fallback no longer reaches the destination endpoint. With
activityEndpoint set and no titleModel on the originating endpoint, it
picked up the destination's, so changing only the credential target
silently changed the model and its cost. Precedence is activityModel, then
the originating endpoint's titleModel, then the run model; the destination
supplies credentials only.

* ✂️ refactor: Confine the Edit-Prefix Offset to Activity Labels

useStepHandler is now byte-identical to dev again. The resume-aware prefix
offset was applied there too, which was more correct in principle -- the
post-resume prefix length is genuinely wrong for run steps as well -- but it
changed index math that EVERY run step flows through, for every user,
including everyone who never enables activityLabel.

That shared correction needed five revisions in two days (isResume, the
stream id, the response message id, clientRequestId, and the SYNC new-row
branch), each passing the full suite and each failing at a boundary only
review found. Carrying it inside an opt-in feature put every user behind
logic with that track record. It belongs in its own change, with tests that
construct the edit-plus-resume states none of the current suites reach.

The offset now applies only where the label handler places its part, so
this PR cannot alter rendering for anyone with the feature off. The known
consequence is recorded in the description: with activity labels ENABLED,
an edited response that reconnects mid-generation can place its label and
its tool cards in different index spaces. That is a bug for opt-in users
rather than a regression for everyone, and it disappears once the shared
fix lands.

submission.editPrefixLength stays: the label path still needs a prefix
length that survives a SYNC replacing initialResponse.content.

* 🧾 fix: Commit Labels Before Billing and Keep Blank Slots Invisible

Round-nine review (all P2, feature-scoped):

- Billing ordering (client.js:409, runtime.ts): usage accounting ran
  BEFORE the slot commit on both generation paths, so the settlement
  deadline could expire during the balance write — charged, then the
  fill dropped as out-of-scope: billed, never shown. `slot.fill` now
  resolves a commit flag, generators register their accounting via
  `deferUsage`, and the hook runs it only after a committed fill.
- Scope gates (client.js:757): the direct-fallback `collect` omitted
  `scopeOpen`; both paths now gate on the OWNING wiring's scope, so a
  pre-pause straggler cannot bill because the resumed generation's
  scope is still open.
- Blank-label grouping (groupToolCalls.ts:81): a blank slot forced a
  flush, splitting adjacent single-call batches into standalone cards
  where the feature-off path merges them. Blank labels now only mark
  the claim boundary — structurally invisible, while a later filled
  label still cannot claim an earlier batch.
- Stale fill indices (wiring.ts:301): the skill-card unshift and the
  hide-sequential filter reshape contentParts before the finalization
  settle, so an in-flight fill emitted its claim-time index against a
  shifted array. Both completion paths now settle label fills before
  any post-run content reshaping (the finally settle stays as the
  error-path net; the second call sees an empty pending list).
- Bounded serialization (runtime.ts:238): `JSON.stringify` fully
  materialized unbounded tool results to keep 200/600 chars per entry.
  A budget-bounded serializer stops at the limit (which also bounds
  cyclic values) and preserves the exact truncate-with-ellipsis output.

Tests: fill/bill ordering + suppression on dropped fills (runtime.spec),
blank-slot merging and claim boundaries (groupToolCalls.test), bounded
serialization equivalence and giant-output truncation (runtime.spec).

* 🧮 fix: Keep Deferred Label Billing Inside the Settle Window

Self-review follow-up to the billing reorder: deferring usage until
after the commit moved it PAST the fill's resolution, so a settle keyed
on fills alone could let finalization flush the usage sink and snapshot
metadata while the label's billing was still in flight — the usage row
would silently miss the message rollup even on the happy path.

The hook now reports its whole detached task (generate → fill →
deferred usage) via a `trackTask` option, wired to the same settle
tracker as the fills, so finalization waits for billing exactly as it
did when accounting preceded the fill. The task never rejects. Pinned
in runtime.spec: the tracked task resolves only after usage collection.

* 🧰 fix: Harden Label Resolution, Output Bounds, and Cache Billing

Round-ten review (all P2, feature-scoped); the sixth finding is the
documented edited+reconnect index-space limitation, answered on-thread
as deliberately out of scope for this PR.

- Rejected-LLM memoization (runtime.ts): the hook cached a rejected
  `resolveLLM()` promise permanently, failing every later batch and
  silently defeating the host resolver's own rejected-cache eviction.
  The memo now evicts on rejection so the next batch retries.
- `current_model` precedence (host.ts): an explicit
  `activityModel: current_model` resolved to `undefined` and then lost
  to a configured `titleModel`. The sentinel now resolves straight to
  the run model; the title fallback applies only when `activityModel`
  is absent.
- Output bounds (runtime.ts): label text was persisted verbatim; a
  model ignoring the 4–9-word instruction (or steered by injection in
  untrusted tool output) could emit thousands of tokens duplicated
  through SSE, the chunk log, persistence, and the UI.
  `normalizeLabelOutput` keeps the first non-empty line, collapses
  whitespace, and hard-caps at 200 chars on both generation paths.
- Cache-token billing (host.ts, client.js): the usage mapper dropped
  cache fields, vanishing Anthropic cache tokens from billing and
  charging OpenAI cache reads at the full input rate. The mapper now
  normalizes Anthropic/OpenAI/LangChain cache shapes into
  `input_token_details`, and the emit + cost path carries them with the
  label endpoint's `provider` (additive-provider adjustment).
- Usage-type union (runs.ts): `TTokenUsageEvent.usage_type` now
  includes the emitted `activity-label` literal; the lone consumer
  keys on `usage_type != null`, so this is type-level completion.

Tests: sentinel/title/explicit model precedence and all three cache
shapes (host.spec), transient-resolution retry and output normalization
with truncation (runtime.spec), the new usage literal (runs.spec).

* 🪗 fix: Let Settled Labels Collapse Void Tools and Keep the Tail Cursor

Round-eleven review (all P2, client-side). Two fixed; the other two
findings restate documented Known limitations (edited+reconnect run-step
index space; parallel-lane collapsible headers), answered on-thread.

- Void-tool auto-collapse (ToolCallGroup.tsx): `allCompleted` keyed
  solely on output truthiness, so a tool that legitimately returns an
  empty string kept its labeled group expanded forever. A settled,
  filled label is itself a completion proof — the PostToolBatch claim
  only happens after every output in the batch returned — so it now
  satisfies `allCompleted`; pending labels keep the group live.
- Trailing-reservation cursor (ContentParts.tsx): a blank label
  reservation at the content tail renders nothing but still counted as
  the last part, stripping the streaming cursor and last-item
  affordances from the last VISIBLE part until the next delta.
  `lastContentIdx` now walks back past empty label slots.

Tests: labeled void-tool group auto-collapses, pending-label group
stays expanded (ToolCallGroup.test).

* 💳 fix: Price Label Cache Correctly, Honor endpoints.agents, Cancel Every Retry

Round-twelve review: four fixed here; the remaining P1 (move the
client.js bridge into packages/api) is an architecture call answered
on-thread for the maintainer.

- Provider on billed entries (client.js, P1): round ten added cache
  details to label usage entries but not `provider`, and `splitUsage`
  treats an unknown provider as additive — re-adding cache_read and
  cache_creation on top of an input count that already contains them,
  double-charging Anthropic/OpenAI cached label calls while the
  streamed cost (which carried the provider) disagreed. Every mapped
  entry now carries the label endpoint's provider.
- endpoints.agents honored (host.ts, client.js): `initializeAgent`
  rewrites `agent.endpoint` to the backing provider, so activity
  settings under the PUBLIC `agents` endpoint — valid config, inherited
  by `agentsEndpointSchema` — were silently ignored. Field resolution
  is now `all` > public endpoint > backing provider/custom, applied to
  both the enable gate and the model/titleModel resolution.
- E2E_LABEL_PORT reaches the YAML (playwright.config.mock.ts): an
  overridden port moved the fake label server and its health check but
  not the generated config's hard-coded 8889 baseURLs, so readiness
  passed while every label request targeted the wrong port. The
  override is now substituted into the generated copy.
- Every retry frame cancelled (useResumableSSE.ts): concurrent label
  retry chains (reservation + fill per slot) overwrote one rAF handle,
  so cleanup cancelled only the newest chain; the rest ran up to 120
  frames past unmount and could apply a stale label to a replacement
  generation reusing the same response id. Outstanding frame ids now
  live in a Set that cleanup drains.

Tests: public-endpoint gate/precedence/all-above-public (host.spec).

* 🖱️ fix: Keep the Last-Part Cursor in Parallel Lanes Too

Round-thirteen review (single P2): `ParallelContentRenderer` computed
`lastContentIdx` from the unfiltered array, so a trailing blank label
reservation — filtered out of every lane — left NO rendered part
carrying the last-part cursor and running-subagent affordances until
the label filled.

The sequential renderer's walk-back is extracted into a shared
`lastVisibleContentIdx` helper (utils/activityLabels) used by both
`ContentParts` and `ParallelContentRenderer`, so the two index spaces
cannot drift again. Behavior pinned in activityLabels.spec: trailing
blank skipped, consecutive blanks skipped, filled label counts,
label-free content unchanged.

* 🧹 chore: Alias the Retry-Frame Set for the Effect Cleanup Lint Rule

* 📏 fix: Let activityCharLimit Reach Tool Inputs

Round-fifteen review: `activityCharLimit` is documented as the
per-entry truncation for tool input AND output, but `buildPrompt`
hard-coded inputs at 200 characters — so raising the setting could
never surface a distinguishing path, query, or operation that appears
past the first 200 characters of a long argument. Inputs now truncate
at the configured limit alongside outputs; the 200-char constant
remains only for the intent line (renamed INTENT_CHAR_LIMIT to match).
Config fidelity pinned in runtime.spec: a 400-char argument survives a
450 limit and truncates under a 50 limit.

The round's other finding is the fifth restatement of the documented
edited+reconnect index-space limitation, answered on-thread with the
prior four cross-references.

* 🤝 fix: No Labels for Pure Handoff Batches

Round-sixteen review: a PostToolBatch containing only `transfer_to_*`
calls claimed a label slot, but transfer parts are never groupable —
the client flushed the handoff card standalone and the label orphaned
into a stray line after it, restating what the card already says.

Two-sided fix:
- Hook (runtime.ts): a batch whose every entry is a transfer call
  claims nothing — no slot, no model call, no `maxPerRun` consumption.
  Mixed batches still label (the header describes the real work).
- Renderer (groupToolCalls.ts): an orphan label whose `tool_call_ids`
  are all transfer calls is dropped instead of rendered standalone,
  covering content persisted before the hook-side skip.

The round's two P1s are repeats answered on-thread: the packages/api
extraction (maintainer-decided follow-up, recorded in the description)
and the sixth restatement of the edited+reconnect index limitation.

Tests: transfer-only batch claims nothing, mixed batch still claims
(runtime.spec); transfer-only orphan label dropped, real-batch orphan
label still renders (groupToolCalls.test).

* 🎛️ fix: Sanitize Label Client Options and Bound the Batch Prompt

Round-seventeen review: two fixed; the other two findings repeat the
maintainer-decided packages/api extraction (follow-up) and the
edited+reconnect index limitation (seventh instance), answered
on-thread.

- Primary-option strip (host.ts): the label client copied the resolved
  `llmConfig` wholesale, so an endpoint whose defaults enable extended
  thinking or carry model-specific output caps forwarded them to the
  (often cheaper) label model — unsupported options failed every label,
  and supported thinking spent real tokens and the settlement window on
  a 4–9 word header. The copy now strips `omitTitleOptions` keys and
  the `modelKwargs` output caps exactly like the title path, restoring
  the Anthropic `clientOptions` carrier by reference so proxy
  `defaultHeaders` still reach label requests.
- Batch prompt budget (runtime.ts): per-entry truncation left the batch
  dimension unbounded — hundreds of parallel calls could build a prompt
  past the fast model's window. The entries section now has a total
  budget (8k chars, scaling with `activityCharLimit` so a raised limit
  still fits several entries); entries past it are skipped without
  paying their serialization cost, and the list notes how many were
  omitted. The first entry always renders in full.

Tests: option strip with header-carrier survival (host.spec); giant
batch bounded with omission marker, small batch untouched
(runtime.spec).

* 🛡️ fix: Keep SSRF Guards on Label Calls, Skip Mixed Handoff Batches

Round-eighteen review: four fixed; the fifth repeats the
maintainer-decided packages/api extraction (eighth instance), answered
on-thread.

- SSRF-safe carrier (host.ts, P1): the sanitize step restored the
  Anthropic `clientOptions` carrier only when `defaultHeaders` existed,
  but for user-provided base URLs `getLLMConfig` stores the guarded
  Undici dispatcher and `redirect: 'error'` there — dropping it
  reopened DNS-rebinding/redirect paths on label calls to
  user-controlled URLs. The carrier (client CONSTRUCTION options, not
  generation params) is now restored whenever present, same reference.
- Primary maxTokens (host.ts): top-level `maxTokens` is not in
  `omitTitleOptions` and survived the strip; the title path deletes it
  explicitly, and a cap sized for the primary model can be rejected by
  the substitute. Deleted on the copy.
- Bounded keys (runtime.ts): the object branch materialized every key
  via `Object.keys` and quoted oversized keys in full before the budget
  check. Enumeration is now lazy (`for..in` + own-property guard) and
  keys slice to the budget before quoting, like string values.
- Mixed handoff batches (runtime.ts, groupToolCalls.ts): the client
  flushes the block at the transfer card, so a mixed batch's label
  orphaned exactly like a pure one. The hook now skips ANY batch
  containing a transfer call, and the renderer drops orphan labels
  covering one (legacy content).

Tests: carrier survival without headers by same reference, maxTokens
strip (host.spec); mixed batch claims nothing (runtime.spec); mixed
orphan dropped, real-batch orphan kept (groupToolCalls.test).

* 🧢 fix: Cap Label Generation, Order the Flag Persist, Detach Settled Listeners

Round-nineteen review: three fixed; the fourth is the ninth instance of
the edited+reconnect index limitation, answered on-thread.

- Generation cap (host.ts): stripping the primary output caps left
  label calls with NO cap at all — `normalizeLabelOutput` bounds what
  persists, not what the provider generates and bills, so a model
  ignoring the 4–9-word instruction (or steered by injected tool
  output) could emit its provider-default output per batch. The
  sanitize step now installs a 256-token label cap (per provider
  family: `maxOutputTokens` for Google-style wrappers, `maxTokens`
  otherwise), after the filter so the omit set cannot remove it.
- Flag-persist ordering (client.js): the `markActivityLabels` write was
  fire-and-forget, so an immediate cross-replica reconnect could read
  the job between the write and the first claim, see neither flag nor
  snapshot label, and skip gap reconciliation. Label emission now
  awaits the (settled-on-failure) persist chain, making "a label event
  exists" imply "the flag is durable" — the race window is gone; only
  the documented double-write-failure residual remains.
- Listener detach (client.js): each HITL approval cycle's wiring adds a
  `once` abort listener to the shared job signal that only an actual
  abort removes; settled segments now detach theirs in
  `settleActivityLabels`, so long multi-approval runs cannot accumulate
  dead closures toward the listener-limit warning.

Tests: the primary cap is REPLACED by the 256-token label cap
(host.spec).

* 🎯 fix: Route the Label Cap Per Model Family

Round-twenty review: the 256-token label cap set maxTokens
unconditionally, but GPT-5+ rejects max_tokens (the OpenAI builder
routes its cap into modelKwargs.max_completion_tokens /
max_output_tokens) and o-series models reject it with no stable kwargs
alternative — every label on those models would have failed. The cap
now mirrors the builder: modelKwargs for GPT-5+ (responses-API aware),
no cap for o-series (title parity; the 200-char persistence bound
still applies), maxOutputTokens for Google, maxTokens otherwise.
Pinned in host.spec for both reasoning families.

The round's other finding is the tenth instance of the documented
edited+reconnect index limitation, answered on-thread.

* ⏱️ fix: Persist the Label Flag at Run Start, Not on the Emit Path

Round-twenty-one review: two fixed; the other three repeat the
maintainer-decided packages/api extraction, the edited+reconnect index
limitation, and the parallel-lane header limitation — all answered
on-thread with their standing decisions.

- Flag ordering, corrected (client.js): sequencing label emission
  behind the flag persist (previous round) delayed the claim-time
  reservation while the shared index offset had ALREADY shifted
  subsequent SDK chunks — reopening the cross-instance
  hole-compaction overwrite the reservation emit exists to prevent.
  The reservation emits immediately again; instead, run start
  (processStream and resume alike) awaits the settled-on-failure
  persist chain, so the flag is durable before any batch can claim a
  label. Same guarantee, zero latency on the emit path.
- Tail-label cursor (ContentParts.tsx): a filled label at the content
  tail is consumed into the group header rather than listed in
  `group.parts`, so the `isLast` check missed it and nothing held the
  streaming cursor until the next delta. The check now includes
  `labelPart.idx`.

* 🔌 fix: Detach Label Abort Listeners Even Without Claims

A segment with labels enabled can end without a single claim (text-only, or handoff batches, which skip labels); the early return in settleActivityLabels skipped the detach added for HITL listener accumulation. The detach now runs on both paths.

* ⚖️ fix: Make the Commit Flag the Sole Billing Authority

Round-twenty-three review: a committed fill racing a late scope close
(user abort or settle timeout during the durable emit) stayed visible
— the part is mutated and persisted before the close — yet the
deferred accounting's scope gates then skipped the charge: a completed
provider call escaping both the label charge and the primary abort
accounting.

The scope gates on the deferred-usage path are removed; the hook's
commit flag is now the single billing authority in BOTH directions. A
dropped fill never reaches the accounting callback (billed-never-shown
stays impossible), and a committed fill bills regardless of when its
scope closed (shown-never-billed now impossible too). The dead
`scopeOpen` payload threading is removed with it; the
`recordActivityLabelUsage` parameter survives, defaulting open, for
callers that own no commit signal.

The round's other finding is the twelfth instance of the documented
edited+reconnect index limitation, answered on-thread.

* 🧮 feat: Bill Labels by Estimate When Providers Omit Usage

Maintainer decision: follow the title convention rather than leaving
label calls unbilled when a provider returns no usage metadata.

The hook now passes a LAZY estimate thunk with the deferred accounting
on the success path — the EXACT prompt the direct path sent (or the
locally built equivalent for the SDK path: same entries, context,
instruction, truncation contract, and continuity headers) plus the
final normalized label. `recordActivityLabelUsage` invokes it only
when no collected entry carries a real token count, counts both texts
with the shared o200k_base tokenizer, and feeds the synthesized entry
through the SAME pipeline (provider-tagged, streamed event, cost,
balance transaction). Real provider usage always wins when present.
The failure path passes NO estimate: a throw before a response bills
only real collected metadata, never a full phantom prompt.

Tests: the estimate thunk carries the exact invoked prompt and final
label; the failure path defers with no estimate (runtime.spec).

* 💵 fix: Estimate From the Raw Completion, Not the Normalized Label

The fallback estimate counted the normalized label (first line, 200-char cap) while the provider generated and would bill the raw output up to the 256-token generation cap — under-recording verbose replies. The estimate thunk now carries the raw pre-normalization text; the persisted label is unchanged. Pinned with a multi-line reply test.

* 🧾 fix: Commit Label Text Only After the Durable Emit, Estimate the Real SDK Prompt

Round review on the billing work: two fixed; the third is the
fourteenth instance of the edited+reconnect index limitation, answered
on-thread.

- Copy-first fill (wiring.ts): the fill mutated the shared content part
  BEFORE its durable emit, so a failed emit left the label text on
  `contentParts` anyway — persistence could save and display a label no
  client ever received and billing (keyed on the commit flag) never
  charged. The new state is staged on a copy; the shared part mutates
  only after the emit succeeds, so content, delivery, and billing move
  together.
- Real SDK prompt for estimates (client.js): the estimate thunk carried
  this module's locally built prompt, but the SDK path frames entries
  differently — the estimated input count was for a prompt never sent.
  Chain-start callbacks (handleLLMStart/handleChatModelStart) now
  capture the prompt the SDK actually rendered, and the deferred
  accounting substitutes it into the estimate when capture succeeded,
  falling back to the local approximation otherwise.
2026-07-29 14:05:47 -04:00
Dustin Healy
f4e0888f14
🕶 feat: Generalize Admin Config Secret Redaction (#14509)
* feat: generalize admin config secret redaction to a field registry

Replace the single hardcoded langfuse.secretKey handling in the admin
config secrets module with a registry (CONFIG_SECRET_FIELDS) so every
credential-shaped config field is encrypted at rest, redacted on read,
and preserved when omitted on a subsequent write.

Registry covers langfuse.secretKey plus speech tts/stt provider apiKeys,
ocr.apiKey, the webSearch provider apiKeys, and the assistants /
azureAssistants endpoint apiKeys. Fields that conventionally hold
${ENV_VAR} references keep those references as plain, visible values;
literal secrets are always encrypted. langfuse.secretKey behavior
(display companion, always-encrypt, array-section handling) is unchanged.

Wire runtime decryption for the consumers that read these values from
the merged app config: resolveConfigSecret in the speech STT/TTS
services and decryptConfigSecret in the Mistral OCR auth loader. Legacy
plaintext literals and ${ENV_VAR} references continue to resolve.

* feat: add masked display companions for every registered config secret

Every non-langfuse field added to the secret registry was missing the
displayPath that langfuse.secretKey already had, so redacted admin reads
returned nothing for those fields instead of a masked value like
sk-mis...Z789. The registry-driven encrypt/redact/preserve/mutation-path
logic in secrets.ts was already field-agnostic; the only backend fix is
setting displayPath on the other 15 registry entries.

Add the matching optional display<Field> companion to each zod schema
in librechat-data-provider (ocr, speech tts/stt providers, webSearch
providers, the shared assistants/azureAssistants endpoint schema) so the
field is typed for consumers, mirroring langfuse.displaySecretKey. DB
overrides are Mixed-typed, so nothing breaks without this at the storage
layer, but the type is needed for any typed consumer of TCustomConfig.

A display path can never be written as a secret: direct writes to it are
rejected, and an ancestor-object write that includes a spoofed display
value alongside or instead of the real secret is overwritten or dropped,
never encrypted or persisted.

* fix: harden the config secret write path against masking and mixing bugs

getDisplaySecretKey disclosed the entire value for any secret of 10
characters or fewer, since the first-6/last-4 mask overlaps or covers
the whole string at that length (e.g. self-hosted LocalAI tokens).
Short secrets are now fully masked instead.

writeSecretIntoSection/writeDottedSecret encrypted a literal secret's
raw string verbatim, including leading/trailing whitespace, so a
padded paste round-tripped with the whitespace intact and a
whitespace-only value was not treated as empty. Literals are now
trimmed before encrypting and masking.

Both functions also returned early on an env-placeholder value without
clearing the display companion, so replacing a literal secret with
${ENV_VAR} left the previous masked value stale in the stored config,
and a client-supplied display value submitted alongside a placeholder
secret was never overwritten. The placeholder branch now clears the
display companion in both the dotted-patch and object-valued write
paths.

* fix: fail closed in Mistral OCR auth when a stored ciphertext can't decrypt

loadAuthConfig fell back to the raw v3: ciphertext string whenever
decryptConfigSecret returned undefined, so a corrupted or otherwise
undecryptable stored secret was sent to the Mistral API verbatim as
the apiKey instead of triggering the existing env-var fallback.

isEncryptedConfigSecret is now exported so the OCR auth loader can
distinguish "this looks like ciphertext and failed to decrypt" from
"this was never encrypted" and treat only the former as empty,
preserving literal and ${ENV_VAR} values exactly as before.

* fix: omit undecryptable TTS provider headers instead of sending "undefined"

openAIProvider, elevenLabsProvider, and localAIProvider built their
Authorization/xi-api-key headers directly from resolveConfigSecret's
return value, which is undefined on a decrypt failure. That produced a
literal "Bearer undefined" header (or an undefined-valued xi-api-key
header) sent to the provider instead of failing gracefully.

Each provider now resolves the key once and only includes the header
when it's non-empty, matching the pattern already used by
azureOpenAIProvider and STTService's providers.

* fix: strip secret-ancestor arrays at any depth, not just the top level

encryptConfigSecrets/redactConfigSecrets only stripped an array-valued
registered-secret ancestor when it appeared as a literal top-level key
(e.g. a dotted "speech.tts.openai" key). A true nested array at any
depth, e.g. { speech: { tts: { openai: [{ apiKey: "sk-secret" }] } } },
made walkToParent return null and silently skip that field entirely,
so the literal secret was stored unencrypted and returned verbatim to
any reader with section-level read access.

pruneSecretAncestorArrays now walks every registered field's ancestor
chain and deletes any array found at any depth before encryption or
redaction runs, closing the gap for both write and read paths.

* fix: migrate legacy plaintext secrets instead of dropping them on preserve

preserveConfigSecrets only restored an omitted secret when the existing
stored value was already v3-encrypted. Every field this PR newly
registers was previously stored as plaintext with no protection at
all, so any deployment upgrading into this registry has real
credentials sitting in Mongo as plaintext today. The first time an
admin edited an unrelated field in the same section (e.g. mistralModel
next to ocr.apiKey), the omitted plaintext secret failed the
"already encrypted" check and was silently dropped instead of
preserved, breaking the integration.

The existing value is now encrypted in place when it isn't already
ciphertext or an allowed env placeholder, so the credential survives
the edit and gets a computed display companion instead of being lost.

* refactor: derive masked-preview companions as <field>Preview

Replace the display*-prefixed companion names (displaySecretKey,
displayApiKey, displaySerperApiKey, ...) with a uniform <field>Preview
suffix (secretKeyPreview, apiKeyPreview, serperApiKeyPreview, ...) derived
automatically from the registered secret path — registry entries no longer
declare a displayPath, and the name never collides with display-label
config fields like modelDisplayLabel.

Legacy langfuse.displaySecretKey companions (the only shipped instance,
with no released reader) are stripped from writes and reads and migrated
to secretKeyPreview on preserve, so stored documents self-clean.

Also reject MongoDB operator segments ($, $[], $[id]) in admin config
field paths: isValidFieldPath previously accepted them, letting a patch
like webSearch.$[].serperApiKey reach patchConfigFields as a positional
update that bypassed secret-path validation.

* fix: translate legacy displaySecretKey to secretKeyPreview on reads

Redaction previously deleted the legacy companion outright, so the first
admin read of a not-yet-migrated document showed no configured-secret
indication until a later write migrated it. Reads now surface the legacy
value under secretKeyPreview (when no new-name preview exists) while still
stripping the legacy key from the response; stored documents migrate for
real on their next write.

* fix: detect runtime ciphertext by full encryptV3 payload shape

Runtime resolution (resolveConfigSecret, mistral OCR auth) now identifies
decryptable values by the exact v3:<32-hex-iv>:<hex> shape encryptV3
produces instead of the bare v3: prefix, so a legitimate literal credential
that merely starts with v3: (e.g. from YAML, which the admin write path
never encrypts) resolves as a literal instead of failing decryption.
Write-side prefix rejection stays broad as spoof/echo defense.

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-07-29 13:55:45 -04:00
Danny Avila
c4d30a096e
🏷️ fix: Re-attribute Agent Content After In-Thread Steers (#14497)
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: Re-attribute Agent Content After In-Thread Steers

* 🏷️ fix: Attribute Post-Steer Resume to the Active Handed-Off Agent

* 🏷️ fix: Re-attribute Post-Steer Resumes in Parallel Sequential Stretches
2026-07-28 23:26:48 -04:00
Danny Avila
3edb497502
📦 chore: bump @librechat/agents to ^3.3.5 (#14506) 2026-07-28 23:23:51 -04:00
Danny Avila
3d2f3a6a18
🧩 fix: Apply Every Entry of Multi-Part Stream Deltas (#14502)
The message and reasoning delta handlers read only delta.content[0],
silently dropping the remaining entries of any multi-part delta — reachable
today via Google server-side tool chunks, which dispatch several reasoning
parts in one event. Every entry now chains through updateContent in order,
with the per-part content index computed against the progressively updated
response so later entries observe earlier state.

Companion to the identical fix in the @librechat/agents aggregator
(danny-avila/agents#350), which keeps streamed and saved content
consistent. Adds multi-entry coverage for both delta types.
2026-07-28 22:26:20 -04:00
Danny Avila
aa357a8e17
🎛️ test: Guard Multi-Steer Injection Across Tool Boundaries (#14498)
* 🎛️ test: Guard Multi-Steer Injection Across Tool Boundaries

* 🎛️ test: Enforce ACK Overlap and Ordered Steer Echo Assertions
2026-07-28 22:19:11 -04:00
Danny Avila
4f5808d9ae
🧪 test: Reasoning-Stream Render Perf Benchmark via react-scan (#14494)
* 🧪 test: Reasoning-Stream Render Perf Benchmark via react-scan

Adds a Playwright benchmark that streams one long, unsplit <think> block
(18k chars — 4x the legacy SplitStreamHandler blockThreshold) plus 6k chars
of markdown through the real mock-model agents pipeline, with react-scan
injected to tally per-component renders. It verifies the legacy content-part
splitting (removed in #10533) is not needed for rendering performance:

- The whole reasoning section lands in ONE think part (a single Thoughts
  toggle) — nothing re-splits it anywhere in the pipeline.
- rAF coalescing bounds the think box to ~1 render per 43 streamed chunks
  (122 renders / 5,290 chunks).
- MarkdownBlock renders stay O(blocks + flushes) (153 renders / 2,092 text
  chunks), not O(blocks x tokens).
- Long tasks during the 13.4s stream: one 96ms task; total render time 885ms.
- Typing after the long transcript leaves transcript components quiet
  (<=2 renders across 40 keystrokes).

Runs against the vite dev server (prod minification strips displayName
assignments, which react-scan needs for naming). react-scan itself is not a
repo dependency: install with `npm i --no-save react-scan` or point
REACT_SCAN_PATH at its auto.global.js bundle.

Also fixes the mock e2e stack for local runs: a developer .env with
CHECK_BALANCE=true leaked through neutralizeCredentialEnv (not
credential-shaped) and made every streaming mock spec fail with a
token_balance violation, since the fresh e2e user has no balance record.
vanillaOverrides now pins CHECK_BALANCE=false.

* 🩹 fix: Address Codex Review — Payload Integrity, Frame Bounds, Proxy Port

- Assert the full 18k-char reasoning payload survives the pipeline: expand
  the Thoughts toggle and compare rendered think text against the source
  (whitespace-normalized), instead of only counting toggles.
- Derive render bounds from elapsed frames (60fps + headroom) rather than
  chunk counts, so the coalescing assertion stays meaningful regardless of
  how many chunks stream before resetPerf; apply the same bound to
  MarkdownBlock.
- Tighten main-thread budgets: worst long task < 250ms and long-task total
  < 10% of stream wall time (baseline: one 51-96ms task per run).
- Pass BACKEND_PORT derived from the configured E2E base URL to the vite dev
  server so its /api proxy follows a non-default app-server port.

* 🧭 fix: Address Codex Round 2 — Typed Global, Drained Observer, Full-Payload Checks

- Declare window.__PERF__ via global Window augmentation; drop the
  as-unknown-as double casts from both perf helpers.
- Retain the longtask PerformanceObserver and drain takeRecords() before
  every snapshot/reset so stalls landing near the final render are counted.
- Start the wall clock at the same instant as the tally reset so frame
  bounds and long-task percentages divide by exactly the measured interval.
- Verify the complete markdown body: every generated section heading
  (exact-match), the exact list-item and table counts, and the generated
  code block — END_MARKER alone only proved the suffix rendered.
- Require positive ThinkingContent/MarkdownBlock render counts so a renamed
  component or dropped instrumentation cannot void the upper bounds.
- Cap cumulative render time at 25% of stream wall time to catch sustained
  sub-50ms work that never surfaces as a long task.

* 🧷 fix: Address Codex Round 3 — Page Clock, Completion Wait, Exact Payload Checks

- Measure the stream interval on the page's own clock: reset stamps the
  start, the snapshot evaluation reads the end, so bounds divide by exactly
  the tallied window including work between marker paint and snapshot.
- Wait for the Stop generating button to hide before snapshotting, so
  generation finalization (usage chunk, terminal events, save re-render) is
  inside the measured interval.
- Compare the rendered think text exactly (edges trimmed only) — internal
  paragraph breaks are user-visible under whitespace-pre-wrap and must
  survive verbatim.
- Verify the markdown prose, not just structure: per-section doubled-sentence
  paragraph and both list-item texts, exact table count with cell values, and
  both generated code lines.
- Derive the vite proxy port via getE2EServerAddress() so implicit ports in
  E2E_BASE_URL (default 80/443) agree between the app server and the proxy.
- Pin react-scan@0.5.7 in the README — thresholds are calibrated against its
  instrumentation semantics.

* 🪛 fix: Address Codex Round 4 — Pre-Send Reset, Count Every Code Block

- Reset the tally immediately BEFORE triggering the send: with a 1ms chunk
  delay, the earliest deltas can render between the response headers
  resolving and a post-send evaluation, which the old order erased from the
  measurement.
- Assert Math.floor(sectionCount / 3) occurrences of both generated code
  lines via code-element locators instead of .first(), so dropped later
  code blocks can no longer pass the payload check.

* 🎛️ fix: Address Codex Round 5 — First-Render Clock, Expanded Box, Typing Budget

- Stamp the wall clock at the FIRST render after each reset (inside
  onRender) so idle request-setup time between reset and stream start never
  pads the frame, long-task, or render-time denominators.
- Seed showThinking=true so the reasoning box streams EXPANDED — the heavier
  live-layout path — and drop the post-hoc expand click.
- Bound the typing phase itself: worst long task < 150ms and cumulative
  render time < 25% of the typed interval, so input lag without transcript
  re-renders still fails.
- Derive the vite dev server host from getE2EServerAddress() alongside the
  port, so a non-localhost E2E base URL keeps the app server, listen host,
  and /api proxy in agreement.

* 🧿 fix: Address Codex Round 6 — Stream-Anchored Clock, IPv6 Proxy, Rate-Free Bounds

- Anchor the stream clock to the first ThinkingContent render — the payload
  opens with reasoning, so that is the first assistant-content paint —
  keeping composer renders and idle request setup out of the denominators.
- Bracket IPv6 HOST values when building the vite /api proxy target in
  client/vite.config.ts; unbracketed ::1 produced an unparseable URL.
- Add an absolute cumulative long-task budget (<300ms) to the typing phase
  so repeated sub-threshold stalls cannot evade the worst-case check or
  dilute the ratio via inflated elapsed time.
- Add chunk-relative companion bounds (renders < chunks/4) for both
  ThinkingContent and MarkdownBlock, and hard-pin MOCK_LLM_CHUNK_DELAY_MS=1,
  so a slower stream can no longer loosen the coalescing assertions.
2026-07-28 22:18:24 -04:00
Ravi Kumar L
f4723220cc
🛡️ fix: validate message feedback payloads (#14500)
* fix: validate message feedback payloads

* fix: enforce feedback rating tag consistency
2026-07-28 22:17:30 -04:00
Danny Avila
23d1ad473d
🍃 fix: Amazon DocumentDB Compatibility for Pipeline-Form Updates (#14495)
* 🍃 fix: Amazon DocumentDB Compatibility for Pipeline-Form Updates

- Rewrite acceptTerms without aggregation-pipeline update + $$NOW (null-guarded first-acceptance claim preserves the original timestamp under concurrent and repeat requests)
- Rewrite decrementTagCounts clamp-at-zero decrement as ordered two-op bulkWrite (clamp before guarded $inc)
- Rewrite extendFilesTTL TTL hold as projected read + per-doc guarded $set via tenantSafeBulkWrite, preserving only-widens/ceiling/cleared-stays-permanent semantics
- Log background index-build failures via Model 'index' listeners (previously swallowed silently, e.g. partialFilterExpression rejection on DocumentDB <5.0)
- Add misc/documentdb live-compatibility harness + assessment (AWS-cited)
- Fix latent file.spec helper bug: createdAt backdating was silently stripped by mongoose immutability

Closes #14488

* 🍃 fix: Harden DocumentDB-Safe Updates Against Cross-Call Races

Addresses Codex review on #14495:
- decrementTagCounts: normalize-null / $inc / clamp-negative op triple so
  interleaved decrements of the same tag converge on max(0, ...) exactly as
  the serialized pipeline did (clamp keys on count < 0, not count < amount)
- acceptTerms: guard the repeat-acceptance fallback with a non-null timestamp
  (exact complement of the claim guard) and retry the claim when a
  config/reset-terms.js reset races between the two updates, so acceptance
  never resurrects a reset cycle without a fresh audit timestamp

* 👷 ci: Suppress Ignored-File Warnings in Changed-File ESLint Run

Changed files under config-ignored paths (packages/data-schemas/misc/**)
emit "File ignored" warnings that fail --max-warnings=0.
2026-07-28 22:14:34 -04:00
Danny Avila
6dae785e31
🌯 chore: Retire Rollup-Era devDependencies After tsdown Migration (#14496)
Removes 26 of the 32 Rollup-era devDependency declarations left behind when
these packages moved to tsdown, plus two stale config references and an
override that went inert in #14483.

- Drop all 8 from `packages/api`, all 8 from `packages/client` (including
  `concat-with-sourcemaps`), and all 10 from `packages/data-schemas`. None of
  their tsdown configs import anything from rollup, and none has a rollup
  script or config file.
- Keep all 6 in `packages/data-provider`. Five of them back the `rollup:api`
  script, which the "Circular dependency checks" CI job runs to surface
  rollup's circular-dependency warnings, and `@rollup/plugin-replace` is
  imported directly by that package's tsdown config.
- Drop `rollup.config.js` (exists nowhere in the repo) and
  `server-rollup.config.js` (real, but never read by the `build` task, so
  listing it only caused spurious cache invalidation) from `turbo.json`.
- Drop the `**/rollup.config.js` glob from `eslint.config.mjs`. It matches
  nothing, and never matched `server-rollup.config.js`.
- Drop the root `svgo` override, dead since #14483 removed
  `rollup-plugin-postcss`, the only thing that pulled svgo into the tree.
2026-07-28 22:08:52 -04:00
Danny Avila
1fce7e1f3c
💬 refactor: Raise ask_user_question Option Label Cap to 280 Chars (#14491)
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: Raise ask_user_question Option Label Cap to 280 Chars

Raise OPTION_LABEL_MAX from 120 to 280 and make every ask_user_question
surface wrap long, model-generated strings instead of overflowing.

* 🪟 fix: Bound ask_user_question Popover to the Viewport

The popover is absolutely positioned, so content taller than the viewport
is unreachable by page scroll. Cap the panel at 60vh with the option list
as the only flexible scroll region, and scroll the keyboard-selected row
into view since selection paints a highlight without moving focus.
2026-07-28 14:23:23 -04:00
Danny Avila
9c95bf445f
🍂 chore: Prune Deprecated Packages From the Dependency Tree (#14483)
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
Removes three of the eight deprecation warnings emitted on `npm install`.

- Drop `@types/winston` from `packages/api` and `packages/data-provider`.
  The published tarball ships no type declarations at all, so `winston`'s own
  types were already being used. Declare `winston` as a devDependency instead,
  since both packages `import type { Logger } from 'winston'` and were relying
  on hoisting to resolve it.
- Drop `rollup-plugin-postcss` from `packages/client`. It is unreferenced since
  the package moved to tsdown, and pulled in `cssnano -> postcss-svgo -> svgo@2`,
  which is the only consumer of the deprecated `stable`.
- Override `test-exclude` to ^8 so `babel-plugin-istanbul` stops resolving
  `test-exclude@6`, which pins the deprecated `glob@7`.

The remaining five warnings (`ldapjs`, `whatwg-encoding`, `node-domexception`,
and workbox-build's `glob`/`source-map`) are transitive with no non-deprecated
version available upstream.
2026-07-28 09:28:06 -04:00
Danny Avila
324584552c
⏱️ feat: Configurable HTTP Server Timeouts (#14481)
* http server config added

* Fix TypeScript compatibility by accepting NodeJS.ProcessEnv directly when applying optional HTTP server timeout configuration.

* fix(api): configure HTTP server timeouts for clustered workers

* 🕰️ fix: Warn When HTTP Timeouts Are Not Enforced

Codex review of the rebased contributor work surfaced two ways these settings
silently do nothing. Both reproduce, and neither was reported to the operator.

Bun accepts the four property assignments and reflects them back, but does not
enforce them: with keepAliveTimeout=100 and buffer=1000, Bun 1.3.13 held a
keep-alive connection past 3s where Node 24 closed it at 1101ms. Since `b:api`
runs the server under Bun, the existing info log confirmed a configuration that
was not in effect. Warn instead.

Node sweeps header/request timeouts on `connectionsCheckingInterval`, a
createServer option that `app.listen()` leaves at 30s, so sub-30s values round
up to it: headersTimeout=2000 returned 408 at 30004ms by default versus 2010ms
with a 250ms interval. Warn on values below the sweep interval rather than
restructure server construction, since every documented value and both Node
defaults already sit well above it. keepAliveTimeout is socket-driven and stays
exact, so it is excluded.

Both caveats documented in .env.example.

* 🩹 fix: Inject Runtime Versions Instead of Mutating `process.versions`

The spec deleted `process.versions.bun` to reset between cases, which failed
typecheck with TS2790: `@types/bun` is a packages/api dependency and augments
NodeJS.ProcessVersions with a required `bun: string`, so the property is not
optional and cannot be deleted. Assigning undefined would fail for the same
reason.

That augmentation also made the production check dishonest: TypeScript saw
`process.versions.bun` as always a string, so `!= null` read as a no-op branch
even though it is correct at runtime under Node.

Both resolved by taking runtime versions as a third injectable parameter,
matching the existing `environment` parameter. Callers in api/server are
unchanged, the narrow `{ bun?: string }` type restores honest narrowing, and
the tests no longer mutate global state, so they assert the same behavior
whether the suite runs under Node or `bun jest`.

* 📏 fix: Stop Claiming a Ceiling on Sweep-Delayed Timeouts

The warning added in 9adc3eb1c said the effective timeout is "up to 30000ms",
which promises a bound that does not hold. Node detects header/request expiry
only on the next connection sweep, so the delay is relative to the deadline
rather than capped by the interval: measured against the default 30s sweep, a
2000ms headersTimeout closed at 30004ms, 15x the configured value, and cases
where the timeout is near the interval did not fire within a 9s window at all.

Reworded to state the mechanism without asserting a ceiling, and to point
operators at values of 30000ms or above for predictable enforcement. Same
correction applied to the .env.example note, which claimed short values "round
up" to the interval.

* ⏳ fix: Clamp Headers Timeout to the Request Timeout

Setting only HTTP_REQUEST_TIMEOUT_MS below the 60s headers default left
headersTimeout > requestTimeout, a pairing createServer rejects outright with
ERR_OUT_OF_RANGE. Assigning the properties after construction skips that
validation, and the mismatch silently defeats the request timeout for a stalled
body: with requestTimeout=4000 and headersTimeout at its 60000 default, a client
that completed its headers and then stopped mid-body was still connected after
12s. Clamping headersTimeout to 4000 closes the same connection at 4017ms.

An earlier round dismissed this after testing partial *headers*, where
requestTimeout does evict on time. The gap only appears once headers are
complete and the body stalls, which is the case these timeouts exist to bound.

Mirrors Node's own rule rather than its constructor default: zero on either side
means disabled and is left alone, and an explicitly configured
HTTP_HEADERS_TIMEOUT_MS that conflicts is warned about before being clamped
instead of failing startup over a config typo.

---------

Co-authored-by: Peter Rothlaender <peter.rothlaender@ginkgo.com>
2026-07-28 09:10:17 -04:00
Danny Avila
f7bc50ae5b
📦 chore: bump @librechat/agents to v3.3.4 (#14482)
Some checks failed
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/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
2026-07-28 08:36:18 -04:00
Dustin Healy
044c134ecf
🤏 fix: Filter Admin Config Reads by Section-Scoped Read Capability (#14472)
listConfigs, getBaseConfig, and getConfig only checked the broad
read:configs capability, so a caller holding nothing but
read:configs:<section> grants got a blanket 403 on all three instead
of a response filtered to the sections they hold. Any deployment
using section-scoped config grants hits this.

Adds hasAnyConfigReadAccess as a cheap pre-flight check covering
broad and section-scoped read and manage grants (manage implies
read), so a zero-access caller still 403s before a DB fetch while a
section-scoped caller gets the response filtered to exactly what
they hold. The same manage-implies-read rule is fixed at its root in
getParentCapabilities so a manage-only caller sees the section they
manage instead of having it stripped after passing the pre-flight.

Resolves every section for a request in one batched
getHeldCapabilities query via getReadableConfigSections instead of
one round trip per section.

Includes AppConfig field-renaming normalization (interfaceConfig,
turnstileConfig, mcpConfig) so the filter checks the canonical
section name rather than the renamed response field, and stops
availableTools from bypassing the filter by gating it on its
filteredTools/includedTools source sections.
2026-07-28 07:38:37 -04:00
Danny Avila
728fc1276e
🔒 fix: Bound /files/usage TTL Hold Instead of Clearing It (#14470)
* 🔒 fix: Bound `/files/usage` TTL Hold Instead of Clearing It

`POST /files/usage` marks queued attachments so the 1-hour upload-window
TTL cannot reap them before the client queue drains. It did this by
calling `updateFilesUsage`, which unsets `expiresAt` outright, turning
every touched upload into a permanently retained file.

The client queue is ephemeral browser state, so this also leaks in normal
use: a closed tab or cleared queue leaves nothing referencing the files,
but their TTL is already gone. The same mechanism let an authenticated
user pin arbitrary owned uploads indefinitely, and the route was excluded
from the file limiters, so the touch was entirely unmetered.

Make the operation match its intent, a renewable hold rather than a
release:

- Add `extendFilesTTL`, which pushes `expiresAt` forward by a bounded
  window in a single owner-scoped `updateMany`. Two filter guards keep it
  safe under client-supplied ids: `$exists: true` so an already-released
  file never has a TTL re-added (that would schedule a live file for
  deletion), and `$lt` so a hold only ever moves the deadline later.
  The owner scope is a required argument, so an unscoped call is a no-op
  rather than a cross-user update.
- `handleFilesUsageRequest` now holds for 24h instead of clearing, and no
  longer increments `usage`, since a queue touch is not a send. The real
  release still happens at drain, where `updateFilesUsage` marks the
  files used against an actual message.
- Give `/usage` its own per-user limiter. Keeping it off the upload quota
  was intentional, leaving it unmetered was not.

Abandoned queues are now reaped on schedule, and a replayed touch can only
ever re-assert the same bounded window.

* 🔒 fix: Anchor the `/files/usage` hold to upload time

Codex review on b687922.

The hold derived each new deadline from `Date.now()`, so a caller touching
once a day advanced it by another 24h every time, far below the rate limit.
That left indefinite preservation reachable and made the PR's replay claim
wrong: the window was bounded per call but not in aggregate.

Anchor the deadline to the file's immutable `createdAt` instead of the
request clock. `extendFilesTTL` now takes a lifetime and sets
`expiresAt = max(expiresAt, createdAt + holdMs)` in an aggregation
pipeline, so the target is a fixed point per file and replay is inert
rather than merely bounded. `$max` keeps the widen-only property and the
`expiresAt: {$exists: true}` filter still refuses to resurrect a released
TTL; `createdAt: {$exists: true}` fail-closes when the anchor is absent.

The update runs with `timestamps: false`: a hold is TTL bookkeeping, not a
content write, and bumping `updatedAt` also made every re-touch count as a
modification, hiding whether the deadline actually moved.

Also drop four `.node_modules-*` symlinks that `git add -A` swept in from
an npm install. They pointed at absolute paths on one machine, so every
other checkout got dangling entries. Added the pattern to .gitignore so a
workspace install cannot reintroduce them.

* 🔒 fix: Track the configured approval window in the `/files/usage` hold

Codex review on 9277620.

`endpoints.agents.checkpointer.ttl` is a positive int with no upper bound,
and its docs invite raising it for longer review windows. It drives the
pending-action expiry, so a run can legitimately stay paused past 24h. The
fixed 24h lifetime would then let Mongo reap an attachment while its
approval was still live, and the later queue drain would send a file that
no longer exists.

Replace the fixed constant with `resolveFilesUsageHoldMs`, which adds the
configured approval window to a 24h baseline covering upload, enqueue, and
the run reaching its pause. The route reads the window from the same
`getApprovalTtlMs(checkpointerCfg)` the pending action uses, so the two
stay in lockstep.

The replay bound is unaffected: the window is a per-deployment constant and
the deadline is still `createdAt + holdMs`, so a replayed touch re-asserts
the same instant and `$max` skips the write. Only an operator config change
moves it, never a client.

* 🔒 fix: Renew the `/files/usage` hold across queued runs, under a ceiling

Codex review on 2bd3c52.

The drain sends one queued item per run completion, and each item starts a
run that may itself pause for the full approval window. Since the hold was
taken once at enqueue and pinned to the upload time, an item several places
back could sit through multiple approval windows and lose its attachment
while its chip and the live approval were still there. Another regression
from this PR: the old `$unset` made retention permanent, so deep queues
happened to work.

The queue is unbounded, so no fixed lifetime covers it. Split the hold into
a renewable window and a ceiling:

  expiresAt = max(expiresAt, min(now + renewMs, createdAt + maxLifetimeMs))

`renewMs` covers one run's wait and is granted from now, so a queue that is
still draining re-asserts it at each transition; `useQueueDrain` now marks
the remaining items' files whenever it pops one. `maxLifetimeMs` is
measured from the immutable upload time and clamps every renewal, so
repeated touches converge on a ceiling instead of advancing per call, which
keeps the replay bound from the previous round intact.

This also tightens abandonment: a queue nobody drains now lapses one
`renewMs` after its last touch instead of surviving to the ceiling.

`useQueueDrain`'s spec gained a QueryClientProvider, since the renewal goes
through react-query.

* 🔒 fix: Renew queued holds on a heartbeat, and stop dropping batches

Codex review on f616bed.

Three gaps in the renewal added last commit:

- `collectQueuedFileIds` returned early at the server's 10-id cap, so a
  remainder holding more than one batch renewed only its first message and
  left the rest on their enqueue-time hold. Collect everything and split
  into capped requests instead of truncating.
- A refused `ask()` restores the popped item, but renewal ran before the
  send and covered only the pre-existing remainder. Since the run-end signal
  is already consumed, nothing would touch that item again. Renewal now runs
  after `ask` and includes the restored item.
- A single run can interrupt for approval more than once, each pause running
  to the configured window, so renewing only at drain transitions leaves a
  gap longer than `renewMs` with no renewal in it. The ceiling cannot help
  when nothing renews.

The third is the same structural gap as the previous round along a new axis:
renewal tied to discrete events loses the file whenever two events are
further apart than the hold. Rather than hook each transition, renew on a
30 minute heartbeat while anything is queued, which is far below the
smallest hold (24h) and so covers any single gap regardless of cause.

Still bounded: every renewal is clamped against the file's upload time, so
the ceiling is unchanged. A queue nobody has open emits no heartbeat and
lapses one `renewMs` after its last touch, preserving the abandonment
behaviour.

* 🔒 fix: Cover the pre-migration queue, first tick, and `/usage/`

Codex review on 892a27d.

- The heartbeat watched only the active conversation id, but `drainNext`
  merges in the `NEW_CONVO` queue, which outlives the URL update: items
  queued during the first turn stay keyed there until that run ends. It now
  renews the union of both, deduped since they are the same atom before
  migration.
- The interval installed without firing, so returning to a conversation
  whose hold was nearly up waited out a full period before the first
  renewal. It now renews immediately, then on each tick.
- Express's non-strict routing sends `POST /files/usage/` to the same
  handler with `req.path === '/usage/'`, so the exact comparison pushed it
  onto both upload limiters. A trailing-slash client would have spent its
  upload quota, and collected file-upload violations, on metadata
  heartbeats. Matching now tolerates the trailing slash.

Firing on effect start also made the drain-time renewal redundant: popping
an item changes the held set, so the renewal effect re-runs on its own. The
one case it cannot see is a refused send, where restoring the item leaves
the set identical, so that branch keeps an explicit renewal and the rest is
removed. Net one request per transition instead of two.
2026-07-28 07:37:26 -04:00
Danny Avila
ea643e8c9c
🔗 fix: Render Shared Links Containing Steers (#14480)
* 🔗 fix: Render Shared Links Containing Steers

The /share/:shareId route mounts outside AuthContextProvider, so any
useAuthContext() on that tree throws and the whole page is replaced by the
route error boundary. SteerPart called it directly, and the MessageIcon tree it
renders reaches Endpoints/Icon, which called it too - so fixing only the first
still died on the icon.

Both now read the user atom instead. AuthContextProvider mirrors the user into
it, so authenticated rendering is unchanged, and the share route reads
undefined rather than throwing.

The two crash sites were invisible because the spec mocked both
~/hooks/AuthContext and MessageIcon. Both mocks are gone: the test seeds the
atom and renders the real icon tree, with a case covering the share route
having neither an auth context nor a user.

* fix: sort test imports and assert the real avatar title

The worktree has no node_modules, so the lint-staged sort-imports hook never
ran on the first commit and CI caught the drift.

The icon assertion also used the wrong value: Endpoints/Icon derives the title
from user.name ?? user.username, so the seeded user renders 'Danny', not the
username.

* fix: keep viewer identity off shared steer avatars

store.user is app-wide and survives navigation, so a signed-in viewer opening a
share link still has an identity in state — reading it for the avatar put the
viewer's face on the sharer's steer. The shared branch now renders the generic
avatar, mirroring Share/MessageIcon, while the label guard already handled the
text.

Also fixes the share test, which passed undefined into a defaulted parameter
and so seeded a user anyway, testing the signed-in path it claimed to exclude.
2026-07-28 07:32:44 -04:00
Danny Avila
52fcc51b36
🌍 i18n: Update translation.json with latest translations (#14460)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
2026-07-27 18:51:03 -04:00
Danny Avila
e0892bb291
🍃 fix: Strip $-Prefixed Schema Keywords Before Persisting MCP Tool Params (#14464)
* 🐛 fix: strip $-prefixed schema keywords from MCP tool params before storage

MCP tools whose inputSchema carries a spec-compliant $schema keyword (or any
other $-prefixed JSON Schema keyword) failed to register: MongoDB rejects field
names beginning with $, so persisting the tool's parameters blob threw
"The dollar ($) prefixed field '...$schema' is not valid for storage".

Normalize the schema when building stored toolFunctions (resolve $refs and drop
$-prefixed keywords via the existing resolveJsonSchemaRefs + normalizeJsonSchema
pipeline). normalizeJsonSchema now strips every $-prefixed keyword, not just
$defs, while preserving property names that happen to start with $.

* fix: recurse through every schema-valued keyword when stripping $ keys

MongoDB rejects $-prefixed field names at any depth, but the normalizer only
recursed through properties, items, additionalProperties and unions, so a
$schema or $comment nested under not, if/then/else, contains, propertyNames,
patternProperties, dependentSchemas or prefixItems survived into the persisted
tool parameters and still failed registration.

The keyword sets are now explicit, covering the single-subschema, map-of-schema
and list-of-schema forms.

A $-prefixed property name is deliberately left alone: it is an argument the
tool actually accepts, so dropping it would silently remove the parameter from
the schema the model sees.

* fix: recurse into draft-07 dependencies and 2020-12 contentSchema

Both are schema-bearing and were absent from the traversal sets, so a nested
annotation survived into the persisted tool parameters and still hit the
MongoDB dollar-prefixed-field failure. dependencies is polymorphic - a value
may be an array of property names rather than a subschema - and that form
round-trips unchanged.

* fix: keep __proto__ entries when normalizing schema maps

Schema-map keys name instance properties, so __proto__ is a legal entry and
arrives as a real own property via JSON.parse. Plain assignment invoked the
prototype setter instead, silently dropping the constraint; entries are now
defined rather than assigned.

* fix: bound MCP schema reference expansion and keep __proto__ arguments

A remote MCP server controls the schema fetched at registration, and sibling
references to the same definition each re-expand because visited is cleared
after resolving - so an acyclic graph where each Dn holds two refs to Dn-1
expands 2^n. At depth 24 that is over 16 million nodes, enough to block the
event loop or exhaust memory before registration finishes.

Resolution now carries a node budget and leaves a reference unexpanded once it
is spent, and assignments use defineProperty so an argument legitimately named
__proto__ is not swallowed by the inherited setter during resolution.

---------

Co-authored-by: Arham Wani <arhamwani765@gmail.com>
2026-07-27 18:50:34 -04:00
Danny Avila
250aca375a
🔗 fix: Resolve MCP Tool-Key Boundary Against Configured Server Names (#14448)
* fix: resolve MCP tool-name delimiter collision at invocation time

MCP tool keys are identified internally as `${rawToolName}${mcp_delimiter}${serverName}`
(delimiter `_mcp_`). Several call sites parsed this back apart with a naive
`toolKey.split(Constants.mcp_delimiter)`, assuming the delimiter occurs exactly once.

When the raw upstream tool name itself contains the delimiter substring - which
happens whenever it's exposed through a gateway that prefixes aggregated tool names by
server (e.g. a gateway's own "gitlab-get_mcp_server_version" for GitLab's
"get_mcp_server_version" tool) - the combined key has the delimiter more than once.
`.split()` then produces more than two segments, and destructuring
`[toolName, serverName]` silently keeps only the first two, yielding a bogus server
name that matches no configured server. Tool listing still worked (a different code
path builds keys directly without re-splitting), but invocation failed with
`Tool {name} not found`, and `filterAuthorizedTools` rejected such keys outright as
malformed.

Add `splitMCPToolKey`, which splits on the *last* occurrence of the delimiter instead:
the server-name half is always LibreChat's own normalized suffix (guaranteed not to
contain the delimiter), while the raw tool-name half is untrusted and may legitimately
contain it. This matches `.split()`'s result whenever the delimiter occurs once, and
correctly resolves the collision case. Update the four call sites that parsed this
manually (`handleTools.js`, `MCP.js`, `mcp.js` controller, `filterAuthorizedTools` in
`v1.js`) plus one in the client (`useVisibleTools.ts`) to use it.

Fixes #14440

* fix: resolve MCP tool-key boundary against configured server names

splitMCPToolKey moves to librechat-data-provider so the client and backend
share one parser, and takes the configured server names when the caller has
them: the longest name the key actually ends with wins, which is exact.

Position alone cannot identify the boundary because both halves may contain
the delimiter. lastIndexOf alone fixes gateway-prefixed tool names but
regresses servers whose own name contains it, which ToolService.spec.js
already covered; the last-delimiter path now only serves as the fallback for
callers with no configured set.

Also converts the remaining first-occurrence parsers that the delimiter fix
missed - mcp/auth.ts (custom user vars silently unresolved), mcp/oauth/events.ts,
agents/initialize.ts, and the three client parsers that labelled tool calls
with the wrong server.

* fix: keep client tool-call labels on first-delimiter parsing

The three client parsers had deliberate, tested first-delimiter semantics
(ToolCall.test.tsx asserts the full server name for 'foo_mcp_bar' and the
synthetic 'oauth_mcp_server' call), and the client has no configured server
list in scope to resolve the boundary exactly, so they are left as they were.

Threads the configured names into the event-driven definition loader so it
resolves the same boundary as the authorization filter that admits the key,
and documents the one case that stays undecidable without provenance.

* fix: resolve tool-key boundary against all configured servers

resolveConfigServers only returns lazily-initialized config overrides -
ensureConfigServers skips unmodified YAML servers - so on a stock deployment
the known-name list was empty and suffix resolution never engaged. Adds
resolveMcpServerNames, which keeps every configured server in the normalized
form tool keys carry, and uses it at the loading, auth-map and definition
sites.

Background-tool eligibility now resolves against all configured names before
testing ephemeral membership, so a non-ephemeral server whose name ends in an
ephemeral one is no longer misclassified, and useVisibleTools resolves against
the server map it already receives.

* fix: use resolved server provenance and one app-config read

createMCPTool now uses the serverName loadTools already resolved for the key
and only parses as a fallback, so an unmodified YAML server whose name
contains the delimiter no longer resolves to the wrong server for auth,
reconnection and callTool.

resolveMcpServerContext derives config servers and all configured names from
a single getAppConfigForRequest, replacing two independent lookups on the
chat startup path, and degrades to empty like resolveConfigServers instead of
aborting tool loading when the config lookup fails.

* chore: drop unused resolveConfigServers import

* fix: forward server provenance on the all-tools path and read config once

createMCPTools builds each toolKey from the server name it already has but did
not forward it, so the sys__all__sys path re-derived it by parsing and bound
an unmodified YAML server whose name contains the delimiter to the wrong auth
and invocation context.

loadAgentTools now resolves the MCP server context once and threads it into
loadTools, replacing the second app-config read it had introduced on the
non-event-driven chat startup path.

* fix: carry resolved MCP server name through tool classification

definitions.ts resolves the server for each key and then dropped it when
building loadedTools, so buildToolClassification re-derived it with a
last-segment split and recorded 'Workspace' for a server configured as
'Google_mcp_Workspace'. The resolved name now rides along on the tool
instance and classification prefers it over re-parsing.

* fix: consume carried server name when extracting MCP servers

extractMCPServers re-derived the name with a last-segment split, so a server
configured as Google_mcp_Workspace resolved to Workspace and its instructions
were silently omitted. Prefers the name carried on the tool definition
instance, falling back to the split.

* fix: fail closed on ambiguous MCP keys when persisting server names

Persisted mcpServerNames grant agent-scoped access to a DB server by name
(ServerConfigsDB.getAccessibleServers), so a wrong guess exposes an unrelated
server to everyone who can view the agent. The last-segment split turned
search_mcp_Google_mcp_workspace into 'workspace'; such keys were previously
rejected outright at agent save, so admitting them opened this path.

Derives a name only from unambiguous single-delimiter keys. This is #12250's
guard moved to the boundary it was actually protecting, instead of blocking
tool admission.

* fix: keep DB server access for multi-delimiter tool keys

The fail-closed guard was wrong for the case this PR exists to fix. This index
only grants DB-backed servers, and DB names are slugs that cannot contain the
delimiter (generateServerNameFromTitle strips underscores), so the trailing
segment is always the real server for them - dropping it cost every consumer
of a gateway-prefixed tool their shared-agent access.

Also gates the MCP server-context lookup on the filtered MCP set, so an agent
with no MCP tools no longer pays an app-config read on startup.

* fix: resolve tool-call display names without breaking OAuth calls

The display parsers could not use the shared boundary parser because their
tested behavior depends on first-delimiter semantics. That constraint only
applies to synthetic MCP OAuth calls, whose tool half is always exactly
'oauth', so everything after the first delimiter is the server even when the
server name carries one.

splitToolCallName special-cases that form and defers to splitMCPToolKey for
real tool keys, so a gateway-prefixed tool now renders its own name and
server while oauth_mcp_foo_mcp_bar still resolves to foo_mcp_bar.

* fix: persist resolved MCP server provenance on agents

Deriving mcpServerNames from the tool key cannot tell a config server's
trailing segment from a real DB server name, so a config server named
a_mcp_b indexed an unrelated DB server b and shared the agent's viewers into
it. Neither string rule works: the suffix guess exposes, and failing closed
drops legitimate DB access for gateway-prefixed tools.

filterAuthorizedTools already resolves each tool's server against the merged
registry config, so it now collects those names and create, update and
duplicate persist them. No extra registry queries: the update path unions the
newly resolved names with what the agent already had, and duplicate replaces
the copied list rather than inheriting the source's servers.

Display parsing also takes the configured names, so a real tool call on a
delimiter-bearing server renders the right server and icon.

* test: teach MCP hook mocks about useMCPServerNames

Three specs mock ~/hooks/MCP with a hand-listed factory, so adding the hook
to ToolCall made useMCPServerNames undefined under test and every render
threw. Returns a stable array so the mock cannot perturb render counts.

* fix: rebuild agent MCP server index from surviving tools

Unioning the prior names kept a server indexed after its last tool was
detached, so viewers of a shared agent retained agent-scoped access to it.
The index is now rebuilt from the tools that survive the edit: a prior name
carries forward only while some retained tool still resolves to it, using the
agent's own persisted names as the candidate set, and the rebuild runs on any
tool change rather than only when a new MCP tool is added.

* fix: keep duplicate indexes on registry fallback and harden the oauth split

Duplication blanked mcpServerNames when the registry was unavailable, because
filterAuthorizedTools grandfathers the source's tools without resolving them -
the copy kept tools it could no longer resolve. Source names now carry forward
for the tools that still point at them.

splitToolCallName also treated any oauth_mcp_ prefix as a synthetic OAuth
call, so a genuine upstream tool by that name resolved to the wrong server. A
configured server name now decides when one matches, since a real key always
ends in its server, and the prefix only breaks ties for unconfigured servers.

* fix: thread configured server names through display parsing

parseToolName and getMCPServerName resolved context-free, so a configured
server whose name contains the delimiter showed the wrong server in grouped
tool summaries and subagent tool labels, and stacked icons missed its entry in
the icon map. Both take the configured names now, supplied by the components
that render them.

Adds the hook to SubagentCall's mock factory: the spec renders the real
component, so an unmocked useMCPServerNames would reach the query with no
provider.

* test: cover the auth-map boundary, server provenance and context fallback

Adds regression coverage for three behaviors this PR changed that no test
exercised: customUserVars resolving under the right plugin key for a
gateway-prefixed tool name (the failure that made these tools loadable but
unusable), the resolved server name reaching createMCPTool instead of being
re-parsed, and resolveMcpServerContext degrading to empty rather than
aborting tool loading when the config lookup fails.

Each was checked against a mutated source to confirm it fails when the
behavior is broken.

* fix: normalize server-name candidates and cover the boundary guard

Tool keys embed normalizeServerName's output while the config is keyed by the
raw name, so callers passing raw keys never matched a server whose name needs
normalizing and silently fell back to the last delimiter. filterAuthorizedTools
now maps normalized names back to their config key, and createMCPTool
normalizes its candidates.

Adds the cases an audit found surviving mutation: a configured name that is a
bare but not delimiter-aligned suffix must not match, an empty candidate list
behaves as no list, and splitToolCallName still falls back to the oauth prefix
when a list is supplied but nothing in it matches.

* fix: keep resolved server names when a non-owner retains MCP tools

The shared-agent path keeps an agent's existing MCP tools verbatim but supplied
no mcpServerNames, so persistence re-derived them and reduced a configured
server like Google_mcp_Workspace to Workspace - which ServerConfigsDB then
treats as a DB server, granting the agent's viewers access to an unrelated one.
Carries the existing resolved names across instead, and clears the index on the
owner path where every MCP tool is removed.

* fix: preserve resolved MCP names for every tools update

extractMCPServerNames was reachable from any caller that writes tools without
mcpServerNames - the Action edit path does exactly that - so a configured
Google_mcp_Workspace was reindexed as Workspace and ServerConfigsDB granted
shared-agent viewers an unrelated DB server by that name.

updateAgent now rebuilds the index from the agent's own resolved names: one
carries forward while a retained tool still resolves to it, and only keys
matching none of them fall back to derivation. Callers are safe by default
rather than by remembering to pass the set.

normalizeServerName moves to librechat-data-provider so the client can match
its candidates against tool keys, which embed the normalized form; the icon map
is keyed the same way since it is looked up with a parsed server name.

* refactor: move MCP context resolution into packages/api

New backend logic belongs in the TypeScript workspace per CLAUDE.md, with /api
kept to a thin wrapper. resolveMCPServerContext now lives in
packages/api/src/mcp/context.ts and takes ensureConfigServers by injection,
since the registry accessor is still legacy-only; the /api function is reduced
to loading the request app config and translating failures into the empty
degrade it already promised.

* test: teach the MCP service mock about resolveMCPServerContext

The spec mocks @librechat/api with a hand-listed factory, so moving the
resolver into that package left it undefined and the wrapper degraded into its
own catch, returning empty config servers. The stub mirrors the real resolver
so these tests still cover what the wrapper owns - loading the request config
and degrading on failure - while the resolution logic is unit-tested in
packages/api.

* fix: only persist an authoritative MCP server index on update

Assigning the resolved set unconditionally pinned the index to [] whenever
nothing authoritative was available - a legacy agent holding MCP tools with no
stored mcpServerNames - which suppressed updateAgent's derivation and stripped
agent-scoped access to its DB-backed server.

The field is now supplied only when the result is authoritative: names were
resolved, or no MCP tool survives so the index genuinely is empty. The
retained-tools branch likewise leaves it unset when the agent has none stored.

---------

Co-authored-by: Jens Schumann <schumajs@gmail.com>
2026-07-27 14:45:38 -04:00
Danny Avila
74f46f90a1
🗺️ chore: Bump PostCSS to 8.5.18 to Patch Source Map Traversal (#14463)
Closes GHSA-r28c-9q8g-f849 (CVSS 7.5, CWE-22), a path traversal in
previous source map auto-loading via sourceMappingURL that allows
arbitrary .map file disclosure. Affected range is <=8.5.17, so the
prior 8.5.13 pin was flagged high by npm audit.

Raises both the root overrides entry, which governs the single copy
in the tree, and the client devDependency floor.
2026-07-27 12:45:55 -04:00
Danny Avila
531fecc82b
🍃 chore: Bump Mongoose to 8.24.1 to Patch Prototype Pollution (#14461)
Closes GHSA-664h-wqgq-64gw (CVSS 6.5, CWE-1321), a prototype pollution
in update casting via a __proto__-prefixed dotted path. Affected range
is >=8.0.0 <8.24.1, so 8.23.1 was flagged by npm audit.
2026-07-27 12:40:14 -04:00
Danny Avila
a53936d273
🧭 test: Cover Agent Handoffs End to End (#14428)
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
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
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
* test: cover agent handoffs end to end

* style: sort handoff imports

* fix: normalize missing agent handoff edges

* chore: update package dependencies and versions in package-lock.json and package.json

* chore: bump agents SDK
2026-07-27 08:47:15 -04:00
unsnow-iac
8374b8416a
📧 fix: Restore invite-user CLI (#14436)
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: restore invite-user CLI

* fix: guard createInvite failure shape in invite-user CLI

createInvite resolves to { message } on error rather than throwing, so the
CLI interpolated it into the register link as [object Object] and emailed
an unusable invite.

* fix: normalize invite email before token creation

findToken lowercases its email query but the Token schema has no lowercase
setter, so a mixed-case address passed to the CLI was stored verbatim and
the resulting invite could never be redeemed.

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-07-26 23:35:50 -04:00
Danny Avila
d8427ffc5e
🛂 test: Cover Tool Approval Workflows End to End (#14427)
* test: cover tool approval workflows end to end

* fix: preserve tool approval state across resume

* fix: preserve agent context in mock stream responses

* fix: preserve nested approvals in collapsed groups
2026-07-26 21:58:25 -04:00
Danny Avila
f3159f9891
🧩 fix: Harden Agent Skill Lifecycles End to End (#14429)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Has been cancelled
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
* test: cover agent skill lifecycles end to end

* style: sort agent skill imports
2026-07-25 08:19:12 -04:00
Danny Avila
73699b5c25
⚡ perf: Reduce Agent Chat Startup Latency (#14423)
* perf: reduce agent chat startup latency

* test: align Redis stream readiness assertions

* perf: overlap remaining agent startup work

* perf: persist initial agent job metadata atomically

* test: add agent startup latency benchmark

* fix: harden resumable agent stream lifecycle

* fix: isolate replacement stream lifecycles

* fix: preserve terminal stream epochs
2026-07-25 07:58:20 -04:00
Danny Avila
cd215150cc
✳️ feat: Claude Opus 5 Support (#14422)
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: Claude Opus 5 Support

- Add claude-opus-5 to Anthropic/Bedrock model lists, token maps, and pricing
- Extend requiresExplicitThinkingDisabled to Opus 5 so thinking-off sticks
- Clamp xhigh/max effort to high when thinking is disabled (Opus 5 400)

* 🪣 fix: Use Bedrock Inference Profiles and Add Vertex Opus Models

Bare `anthropic.` Claude 4+ IDs are not invocable on-demand via Converse:
Bedrock rejects them with "Retry your request with the ID or ARN of an
inference profile that contains this model." Verified live against
us-west-2 for Fable 5, Opus 5, Opus 4.8, Sonnet 5, Sonnet 4.6, Opus 4.6,
Sonnet 4.5, Haiku 4.5, and Opus 4.1. Switch those defaults to the
`global.` profile (no regional pricing premium); Opus 4.1 has no global
profile, so it uses `us.`.

Also add the modern Opus family to the Vertex defaults. `loadEndpoints`
swaps the shared Anthropic list for the Vertex model names, so Opus was
invisible to every Vertex deployment that did not enumerate models by hand.

* 📋 chore: Cover Opus 5 Gaps From PR #14420

Picks up items from the parallel community PR by @jona7o:

- Add claude-opus-5 to the librechat.example.yaml Vertex example (both the
  legacy array and the deploymentName map), which already lists Fable 5
  and Opus 4.8
- Mention Opus 5 in the configureReasoning doc comment, and note that its
  early return is why the effort cap is enforced by the caller
- Assert Opus 5 carries no long-context premium pricing
- Cover the Sonnet 5 negative case for the effort cap, and the persisted
  disabled-object round-trip carrying an effort

* 🌍 docs: Warn That Vertex Regional Endpoints Reject Modern Models

Anthropic serves Sonnet 4.6 and earlier on specific Vertex regional
endpoints; newer models (Opus 4.7+, Opus 5, Sonnet 5, Fable 5) require
`global` or a multi-region location and 404 on a specific region. The
`us-east5` default therefore cannot serve the Opus models added here, nor
the Sonnet 5 entry that predates this branch.

Documents the constraint at all three places an operator sets the region,
and at the fallback itself. Leaves the default unchanged: switching it to
`global` would silently alter data routing and residency for existing
deployments, which is a separate call.

* 🩹 fix: Restore PDF Exemption for Undated IDs and Gate Vertex Defaults

Two issues raised in review:

- BEDROCK_CLAUDE_4_PLUS_RE required a `-` after the major version, so it
  matched `claude-opus-4-8` but not undated IDs like `claude-opus-5`.
  Those models silently lost the Claude 4+ PDF exemption and fell back to
  the 4.5 MB limit. Sonnet 5 and Fable 5 were already affected before this
  branch; Fable/Mythos were also missing from the family alternation.

- The Vertex defaults advertised models that only `global` and the
  multi-region locations serve, so a default `us-east5` deployment listed
  Opus choices that 404 on first request. Filter the built-in defaults by
  configured region instead of changing the region default, which would
  alter data routing for existing deployments. An explicit `vertex.models`
  list is the operator's choice and is never pruned.

* 🧩 fix: Match Bare Claude IDs in the Bedrock PDF Exemption

An application inference profile maps a LibreChat model ID with no
`anthropic.` segment, so `claude-opus-5` failed the Claude 4+ check and
fell back to the 4.5 MB PDF limit. Make the prefix optional and accept
both segment orders, mirroring BEDROCK_CLAUDE_4PLUS_THINKING in
librechat-data-provider, which matches on the family token for exactly
this reason.

Only reached for the Bedrock provider, so the looser prefix cannot leak
into other endpoints. Verified Claude 3.x, Nova, Llama, Cohere, and
Mistral IDs still fall through to the default limit.

* 🧹 fix: Drop Retired Claude 3.5 Models From Bedrock Defaults

The three Claude 3.5 entries reached end of life at AWS and return
ResourceNotFoundException in every prefix form (bare, `us.`, `global.` —
verified live against us-west-2), so selecting one was a hard error.
Their modern equivalents are already in the list: Sonnet 5 / Sonnet 4.6
supersede the 3.5 Sonnets, and Haiku 4.5 supersedes 3.5 Haiku.

Every remaining Anthropic default is now live-verified invocable.
`.env.example` swaps its retired example ID for Haiku 4.5.

* 🔒 refactor: Narrow Effort-Clamp Types Instead of Asserting

Both clamp sites reached into loosely-typed containers with assertions:
llm.ts used an `as unknown as { type?: string }` double assertion to read
the thinking type, and the Bedrock parser cast `output_config` to
`{ effort?: unknown }` before confirming it was an object. CLAUDE.md's
type-safety rules call for narrowing over both.

Adds `isThinkingDisabled` and `clampOutputConfigEffort` to
librechat-data-provider, using `in`-operator narrowing and a type
predicate so no assertion is needed at all. Both call sites now share one
implementation rather than duplicating the clamp.

Behavior is unchanged; existing clamp tests cover it.
2026-07-24 21:45:32 -04:00
Danny Avila
6c97a7f467
♾️ fix: Preserve Resumable Stream Ordering Across Turns (#14411)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Has been cancelled
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Has been cancelled
GitNexus Index / index (push) Has been cancelled
GitNexus Index / post-index (push) Has been cancelled
* fix: preserve resumable stream ordering across turns

* chore: sort stream regression imports

* test: mirror sliding sequence ttl in publisher mock

* fix: prevent duplicate early stream replay

* fix: preserve replay frontier when sync fails
2026-07-23 13:25:38 -04:00
Danny Avila
21dc4a2ef4
🎯 fix: Correct Off-by-One Rail Scrub After Pinning the Terminus (#14409)
Some checks failed
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
GitNexus Index / post-index (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
Pinning the scroll-to-bottom rib moved it out of the column, but scrubTo
kept enumerating ribs from the nav (messages + terminus) while measuring
the fraction against the column, which now spans the messages alone. Every
drag position mapped one rib late: pointing at the middle of the rail
scrolled to the message below the rib under the cursor.

Enumerate the column's own ribs for the proportional mapping and reach the
terminus by dragging past the column's bottom edge, where it now sits.
2026-07-23 11:48:45 -04:00
Danny Avila
142973e7e8
🌍 i18n: Update translation.json with latest translations (#14406) 2026-07-23 10:03:12 -04:00
Danny Avila
60eba76375
🫙 fix: Preserve Loaded Message Content When Resume Snapshot Is Empty (#14399)
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: Keep Loaded Message Content When a Resume Snapshot Is Empty

The sync handler guarded on `data.resumeState?.aggregatedContent` and then
assigned it unconditionally. An empty array is truthy, so a resume snapshot
carrying no content overwrote the content the messages query had already
loaded, leaving the message rendering as a bare cursor with no way to recover.

Treat an empty snapshot as non-authoritative: keep the loaded content and let
live deltas take over. A snapshot that carries content still wins, unchanged.

This is defense in depth rather than a root cause. A resume snapshot goes empty
when the conversation's job is replaced mid-flight (#14348) — because
`streamId === conversationId`, a second submission calls `createJob` on the same
key while the first run is still streaming. With this change that failure
degrades to stale-but-visible instead of destroying content already on screen.

* 🔒 fix: Scope Resume Content Preservation to Identity-Matched Responses

Codex P2: on a regenerate reload before the new run aggregated anything, the
server reports an empty snapshot under a response id not yet in the loaded
history. The parent-based fallback then lands on the answer being REPLACED, and
preserving its content seeded `syncStepMessage` with the stale response, so the
regenerated run's deltas appended to it instead of starting blank.

Preserve loaded content only when the row was matched by the server's declared
`responseMessageId` — the sole case proving the row belongs to this generation.
A fallback-matched row keeps the previous clear-on-empty behavior.

* 🧱 fix: Only Preserve Resume Content When the Row Actually Has Parts

A matched row with no `content` array would have been assigned `undefined`
instead of the snapshot's array. `MultiMessage` branches on `message.content`
truthiness to pick its renderer, and `[]` is truthy while `undefined` is not, so
that would have silently switched a streaming row from the content-parts
renderer to the text one.

Preserve only when the loaded row has a non-empty content array — the case the
guard exists for. Narrows the divergence from prior behavior further: it now
applies solely when there is real content to protect.
2026-07-23 08:22:17 -04:00
Danny Avila
30ae414911
📌 fix: Pin Scroll-to-Bottom Rib in Message Nav (#14397)
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: Pin Scroll-to-Bottom Rib in Message Nav

Render the terminus rib outside the scrolling rail, between the column
and the down chevron, so the scroll-to-bottom affordance stays in view
no matter how far the rail has scrolled.

- scrubTo enumerates ribs from the nav so drag-to-bottom still lands on
  the terminus
- the pinned rib drives the shared preview itself on hover and focus,
  since it is no longer covered by the column's pointer magnification

* 🖱️ fix: Keep Drag-Scrub Startable From the Pinned Terminus

Pointer-down on the pinned rib no longer reaches the column's handler now
that it renders outside the scrollport, so wire the same drag-start to the
wrapper. Dragging up from the bottom dot scrubs the thread again.
2026-07-22 22:13:29 -04:00
Danny Avila
00c5a747e9
🧵 feat: Native Background Execution for Code Interpreter Tools (#14386)
* 🧵 feat: Native Background Execution for Code Interpreter Tools

* 🩹 fix: Address Codex Round 1 (fallback dedupe, harvest failure, handle parsing)

* 🩹 fix: Live Completion Marker + Unkeyed Attachment Dedupe (Codex Round 2)

* 🎨 chore: Sort Imports + Widen Marker Type Comparison (CI)

* 🩹 fix: Stale-Harvest Guard, Error Marker Status, Faster Anchor Retry (Codex Round 3)

* 🧹 refactor: TS Harvest Module, Claim-Neutral Timestamps, Error Parity (Codex Round 4)

* 🩹 fix: Dispatch-Ordered Stale Guard, Foreground Downgrade, Error Wrapper Parity (Codex Round 5)

* 🩹 fix: Retry Past Unfinished Rows + Per-Call Attachment Dedupe (Codex Round 6)

* 🩹 fix: Writer-Dispatch Ordering, Scoped Live Upserts, Reaped-Task Wrapper (Codex Round 7)

* 🩹 fix: Wildcard toolCallId Matching for Bare Attachment Updates (CI)

* 🩹 fix: Claim-Insert Dispatch Stamp (Schema-Backed) + Scoped Status Markers (Codex Round 8)

* 🩹 fix: Pre-Write Ownership CAS + Agent-Scoped Part Patching (Codex Round 9)

* 🩹 fix: Insert-Path Ownership CAS + Agent-Routed Attachments (Codex Round 10)

* 🩹 fix: Agent-Scoped Marker Ids and Attachment Dedupe (Codex Round 11)

* 🩹 fix: Atomic File Commit and Sibling Preview Fan-Out (Codex Round 12)

- Replace the two-step claim-confirm CAS with an atomic conditional updateFile: the ownership predicate (no sourceDispatchedAt, or <= this write's dispatch order) moves into the update filter, removing confirmCodeFileOwnership and the lost-update window between check and write
- Thread agentId through createDownloadFallback so fallback download rows scope to the emitting agent like primary rows
- Fan terminal preview overlays out to every live attachment sharing the file_id in useAttachmentPreviewSync (sibling tool calls no longer stick on pending)
- Restore background artifacts through toStoredArtifact so the size bound applies on re-anchor
- Apply filterAttachmentsForPart to grouped tool-call attachments in ContentParts so handoff agents with colliding provider call ids do not cross-contaminate groups

* 🩹 fix: Agent-Scoped Live Upserts and Monotonic Dispatch Stamps (Codex Round 13)

- Scope the SSE attachment upsert and the useAttachments DB/live merge by agentId with the same wildcard semantics as toolCallId: distinct non-null agentIds stay separate entries, so handoff agents sharing a claimed file_id and a repeated provider tool id (call_0) no longer merge over each other's cards
- Extend the attachment identity key to fileKey::toolCallId::agentId and register less-specific key variants so bare and agent-less live records still dedupe after overlay
- Stamp background task createdAt from a strictly-increasing per-process dispatch counter: raw Date.now() can tie for same-millisecond dispatches and the stale-output guard accepts equal stamps (needed for idempotent re-commits), which would let an older task overwrite a newer task's committed file
2026-07-22 22:13:15 -04:00
Danny Avila
5af12c722e
🎭 ci: Scope Playwright Runs to Relevant Changes (#14388)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
2026-07-22 12:23:39 -04:00
Danny Avila
dbb771aa7c
🚦 ci: Gate Playwright Runs to Maintainers (#14385)
* ci: gate Playwright runs to maintainers

* ci: guard pull request context explicitly
2026-07-22 12:23:02 -04:00
Danny Avila
ad5bb477af
🎞️ fix: Surface Clear Error for Unprocessable Gemini YouTube Videos (#14396)
Google rejects a YouTube video it cannot ingest with a generic
`400 INVALID_ARGUMENT` that names no cause, which LibreChat relayed
verbatim. Attribute the failure using request context instead: when a
Google/Vertex turn carried an injected YouTube video part and the
provider returns that generic rejection, map it to a typed error the
client localizes.

Verified against the live API: a public 9h15m video is refused this way
on gemini-2.5-flash, 3.5-flash, 3.5-flash-lite and 3.6-flash, including
at MEDIA_RESOLUTION_LOW, while a short video with an identical payload
succeeds. Duration is the dominant trigger; region and access
restrictions return the same response, so the copy leads with length
without overclaiming.

A duration preflight was evaluated and skipped: oEmbed does not expose
duration, leaving only watch-page scraping — a blocking call against
undocumented markup from rate-limited datacenter IPs that would fail
open and still need this mapping underneath.
2026-07-22 12:11:06 -04:00
Danny Avila
337facb4f0
✋ fix: Enable Submit on First Tool-Approval Decision (#14393) 2026-07-22 12:09:53 -04:00
Danny Avila
4c0ac8844c
🪆 fix: Preserve Nested Subagent Delegation (#14392)
Co-authored-by: Sien Nuyens <sien.nuyens@ixor.be>
2026-07-22 09:30:53 -04:00
Danny Avila
af7b2761eb
🪟 perf: Virtualize Search Results and Stop the Per-Query Remount (#14352)
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
* 🪟 perf: Virtualize Search Results and Stop the Per-Query Remount

* 🔧 fix: Type-safe globalThis cast in Search route test

* 🔑 fix: Stabilize Search Row Keys and Harden Virtualized Result Edges

Address Codex review findings on the virtualized search results view:

- Key rows by messageId (outer React key + CellMeasurer + cache keyMapper all
  aligned) so React reconciles by message, not scroll slot.
- Compare title and conversationId in areSearchMessagePropsEqual so a rename or
  refetch that keeps text/id intact still re-renders the row.
- Recompute cached heights on same-length content changes (file previews
  resolving, refetch) and on font-size changes.
- Keep the aria-live announcement on the empty-results branch.
- Don't paginate the outgoing query while the user is still typing.
- Show the spinner during the initial debounce instead of a blank route.

* 🧵 fix: Reset Scroll, Remeasure Growth, and Footer-Pad Virtualized Search

Address the second Codex round on the virtualized search results view:

- Reset the List scroll to the top on a new query (results stay mounted via
  keepPreviousData, so the List otherwise keeps the previous scrollTop and can
  open a new search mid-list); a font-size change keeps the user's place.
- Re-measure a row when its content later grows/shrinks (tool/code output
  expands, a late image loads) via a ResizeObserver that clears just that row's
  cached height and recomputes from it.
- Give the load-more throttle trailing:false and cancel it when the query
  changes, so a queued fetch can't page a stale search.
- Add a fixed trailing spacer row so the last result clears the bottom
  gradient/spinner overlay.

* 🫥 fix: Gate Stale Search Results on Refetch State, Not Just Typing

Address the third Codex round: `isTyping` clears when the debounce publishes the
new query, but `keepPreviousData` keeps the old pages mounted until the new
request lands, leaving a window the typing-only guards missed.

- Derive `showingStale = isTyping || isPreviousData` and gate both the dimming
  and pagination on it, so the outgoing results stay dimmed and don't page while
  the new query is still fetching.
- Compare `unfinished` in areSearchMessagePropsEqual so a finish/cancel that
  changes only that flag re-renders SearchContent's incomplete-response notice.

* 📐 fix: Invalidate Row Height Against the Cache and Compare clientTimestamp

Address the fourth Codex round on virtualized search:

- Compare each ResizeObserver height against the cached row height instead of
  skipping the first callback, so a cached/fast-loading image that is already
  taller than CellMeasurer's mount measurement still invalidates the stale
  height (no more overlap/clipping of following rows).
- Compare clientTimestamp in areSearchMessagePropsEqual, since the row timestamp
  falls back to it when createdAt is absent.

* 🕳️ fix: Spinner Over False Nothing-Found for Stale Empty Search Data

Address the fifth Codex round: when the previous search had zero matches,
keepPreviousData holds those empty pages (isPreviousData, isLoading false)
during the new request, so the loading gate missed it and flashed a false
"nothing found". Gate the spinner on `showingStale` too, not just isLoading/
isTyping.
2026-07-22 04:26:47 -04:00
JOJO
8751cc1c5c
🔗 fix: Preserve resource owner access when sharing (key share diff by stable id) (#14317)
* 🔗 fix: Preserve resource owner access when sharing (key share diff by stable id)

The share dialog diff (GenericGrantAccessDialog.handleSave) keyed added/removed
principals by `idOnTheSource`, which is inconsistent for the same user across
sources: getResourcePermissions returns `userInfo.idOnTheSource || _id` (the
external oid for OpenID/Entra users) while the people-picker returns the local
`_id`. The resource owner then appears in both `updated` and `removed`, and — since
updateResourcePermissions applies grants (upsert) before revocations (delete) — the
owner's own ACL entry is deleted when they add anyone to the share list. They then
get 403 on GET/edit/re-sharing their own resource.

Extract the diff into a pure computeShareChanges() helper keyed by
`id ?? idOnTheSource` (stable local id when present, external oid fallback for
principals not yet synced locally, e.g. unsynced Entra groups/users). Add unit tests.

Not reproducible with local-only users, where idOnTheSource falls back to _id and
both sources agree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* 🔧 fix: address review — dedupe share diff by principalKey; drop test assertion

- computeShareChanges now diffs over the de-duplicated map values, so a principal
  that appears more than once in the input (possible while the add/dedupe path still
  keys on idOnTheSource) is never emitted multiple times in updated/removed.
- Drop the `as TPrincipal` assertion in the test helper — the literal is structurally
  compatible with TPrincipal, so TypeScript validates the shape directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 04:21:28 -04:00
Danny Avila
ca6ffb33fd
📦 chore: Update @librechat/agents to v3.2.68 (#14380)
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
2026-07-21 21:40:54 -04:00
Danny Avila
cbaa2fe2e3
⚡ feat: Add Gemini 3.6 Flash and Gemini 3.5 Flash-Lite Support (#14369)
* ⚡ feat: Add Gemini 3.6 Flash and Gemini 3.5 Flash-Lite Support

Adds first-class support for Google's Gemini 3.6 Flash (`gemini-3.6-flash`)
and Gemini 3.5 Flash-Lite (`gemini-3.5-flash-lite`) for both the Gemini API
(AI Studio) and Google Cloud/Vertex integrations.

- Context window (1M) in googleModels; API + cache pricing in tx.ts.
- Model dropdown (config.ts) and GOOGLE_MODELS examples for both integrations.
- Generalize the Gemini 3.5 Flash overrides into a flash-family handler that
  strips deprecated temperature/topP/topK and applies each model's default
  thinking level (3.6 Flash: medium, 3.5 Flash-Lite: minimal), with
  longest-prefix resolution so flash-lite does not collide with flash.

Ref: https://ai.google.dev/gemini-api/docs/latest-model#api-changes-and-parameter-updates

* 🩹 fix: Strip unsupported penalty params for Gemini Flash family

Gemini 3.6 Flash, 3.5 Flash-Lite, and 3.5 Flash reject presencePenalty/
frequencyPenalty with HTTP 400 ("Penalty is not enabled for this model",
verified live). These pass through llmConfig via knownGoogleParams, so add
them to the flash-family strip list alongside the deprecated sampling params.

* 🩹 fix: Strip Flash-blocked params on custom Google endpoint path

For custom OpenAI-compatible endpoints with defaultParamsEndpoint=google,
getOpenAIConfig strips Flash-blocked params via getGoogleConfig but then
transformToOpenAIConfig re-applies raw addParams, undoing the strip. Filter
addParams through stripGeminiFlashBlockedParams before the transform so the
deprecated sampling / rejected penalty params cannot reach the provider.

* 🔧 chore: Update sharp package to version 0.35.3 in package-lock.json, api/package.json, and packages/api/package.json

* 🔧 chore: Update dependencies in package-lock.json to latest versions for @google/genai (2.13.0), @hono/node-server (1.19.14), fast-uri (3.1.4), hono (4.12.31), and svgo (2.8.3)

* 🔧 chore: Update dependencies in package.json and package-lock.json for @librechat/agents (3.2.67), @opentelemetry/sdk-node (0.221.0), and add new dependencies for @opentelemetry/propagator-jaeger (2.10.0) and protobufjs (7.6.5). Update monaco-editor version in client package.json to 0.56.0.

* 🔧 chore: Upgrade turbo package to version 2.10.5 in package.json and package-lock.json, and update schema reference in turbo.json

* 🩹 fix: Resolve CI breakage from bundled dependency bumps

Not related to the Gemini models — both are fallout from the dep bumps on
this branch:
- monaco-editor 0.56 changed IEditorHoverOptions.enabled from boolean to
  'on' | 'off' | 'onKeyboardModifier'; update ArtifactCodeEditor to match
  (mirrors the sibling occurrencesHighlight/matchBrackets pattern).
- sharp 0.35.3 fails resize+encode on a degenerate 1x1 PNG (vipspng: libpng
  read error); the provider-file e2e fixture was 1x1, so use a 16x16 PNG.
  Normal images are unaffected (verified 64x64 resize/encode/jpeg all OK).

* 📝 docs: Correct e2e image-fixture comment (bad IDAT CRC, not a sharp bug)

Root cause was the old 1x1 fixture's corrupt IDAT CRC (verified: IHDR/IEND
CRC OK, IDAT CRC BAD), which sharp 0.35.3's stricter libpng correctly rejects.
Not a dimension/resize edge case and not a sharp bug; comment now reflects that.
2026-07-21 21:14:11 -04:00
Danny Avila
1dd7121d71
🌍 i18n: Update translation.json with latest translations (#14376) 2026-07-21 20:36:35 -04:00