[pull] main from danny-avila:main - #248
Merged
Merged
Conversation
…15612) `resolveImageMimeType` returned `image/heic` for any heif container whose compression was not `av1`, which included the case where sharp reported no compression at all. `Metadata.compression` is optional, so unreported is a real outcome, and answering it with HEIC is a guess — an AVIF read back without its compression would be recorded as HEIC. That is the failure #15606 exists to prevent, reproduced inside the fix for it, and it contradicted the documented contract of returning `undefined` rather than naming bytes that cannot be identified. Unreported or unrecognized compression now yields `undefined`, which leaves `saveBase64Image` on the declared type: still only a claim, but the caller's claim rather than one invented here.
* Remove parameters from UI when parameter is dropped through dropParams * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Addressed copilot and codex review suggestions * 1. Normalize drop parameter names in client/src/components/SidePanel/Parameters/Panel.tsx Admin-configured dropParams for Azure/OpenAI-compatible custom endpoints use the effective backend field names (maxTokens, topP, frequencyPenalty, presencePenalty), but the panel's filter compared them against the UI's snake_case keys (max_tokens, top_p, etc.), so the controls stayed visible and silently discarded whatever the user set. Added a dropParamsBackendToUIKey map (in packages/data-provider/src/parameterSettings.ts) and normalize each dropParams entry through it before filtering. 2. Apply the same drop-parameter filtering in client/src/components/SidePanel/Agents/ModelPanel.tsx The Agent builder's model panel built its parameter list independently and never consulted endpointsDropParamsMap at all, so it kept offering controls for Azure/custom providers whose values the backend drops. Reused the same resolution logic (including the new backend→UI key normalization) there, sourced from useGetStartupConfig. 3. Remove any escapes from Azure fixtures in packages/api/src/app/config.test.ts Three tests built azure groupMap/modelGroupMap fixtures via ... as any as AppConfig['endpoints'], bypassing type checking for that shape. Replaced them with two typed helpers, createAzureGroupMap and createAzureConfig, built from the existing TAzureConfig/TAzureGroupMap/TAzureModelGroupMap types, so the fixtures are now fully type-checked with no any. * HAL-1081 Deploy librechat open 1. Gate the backend-name alias to OpenAI-compatible parameter sets (packages/data-provider/src/parameterSettings.ts) dropParamsBackendToUIKey had been applied unconditionally, rewriting a dropped topP to top_p even for a custom endpoint whose defaultParamsEndpoint is anthropic/google (or a native bedrock-* endpoint) — where topP is the UI key, so the rewrite broke hiding the control. Replaced the plain map with resolveDropParamsUIKeys(dropParams, endpointKey), which only aliases backend names for OpenAI-compatible endpoint keys (openAI, azureOpenAI, custom, openRouter) and passes native-provider keys through unchanged otherwise. Updated both Panel.tsx and ModelPanel.tsx to call it with overriddenEndpointKey, and added a resolveDropParamsUIKeys test suite in parameterSettings.spec.ts. 2. Prune stale model_parameters when a control becomes hidden (client/src/components/SidePanel/Agents/ModelPanel.tsx) When a parameter an agent already had set gets added to its endpoint's dropParams, the control disappeared but the value stayed in model_parameters and was saved unchanged by composeAgentUpdatePayload — invisible to the user and silently reactivated if the endpoint later stopped dropping it. Added a useEffect, mirroring the conversation panel's existing pruning effect, that strips any model_parameters key no longer present in the currently-visible parameters list. Also fixed a pre-existing test regression (missing useGetStartupConfig mock) and added two tests covering pruning-on-drop and value retention when the control stays visible. * fix: Preserve parameters for unknown agent providers * fix: Sanitize agent parameters on submission * fix: Preserve agent model overrides during pruning --------- Co-authored-by: Marc Amick <MarcAmick@jhu.edu> Co-authored-by: MarcAmick <5194465+MarcAmick@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
* fix: Repair BYOM live integration * fix: Isolate mutable tool schemas
* 🧹 fix: Stop Undeletable Files Starving the Retention Sweep `getExpiredFiles` returns the oldest `expiredAt` first, capped at `limit`, and `processDeleteRequest` leaves the record in place when storage deletion fails. Nothing records the failure, so the same files come back at the head of the next batch an hour later, forever: no backoff, no cap, and — once `limit` of them cannot be deleted — no file that expires afterwards is ever swept again. On the deployment behind #15511 that is ~29k stranded objects permanently occupying a 100-slot queue, which is why fixing the Code Interpreter side alone (LibreChat-AI/code-interpreter#85) would not have resumed deletion there. Failures are now recorded on the file. `deletionRetryAt` holds it back with a backoff doubling from one sweep interval to a day, and `deletionAttempts` retires it from the query entirely once it reaches `FILE_RETENTION_SWEEP_MAX_ATTEMPTS` (10, so roughly five days of retries). Both fields are absent on existing records and absence means "never attempted", so nothing already in the collection changes eligibility. The record itself is kept rather than deleted — the reference is what an operator needs to reconcile a bucket the sweep could not clear, and dropping it would restore the silence that made this leak invisible. Raising `FILE_RETENTION_SWEEP_MAX_ATTEMPTS` re-admits everything previously given up on, which is the supported way to resume once the storage-side failure is fixed; the give-up is logged with that instruction. The counter is incremented server-side with `$inc` so concurrent sweeps on separate nodes cannot overwrite each other's progress toward the cap, and a failure to record a failure is logged and skipped rather than aborting the rest of the batch. * 🧭 fix: Delete Code Environment Files Through the Route That Exists `deleteCodeEnvFile` tried `/sessions/:sid/objects/:fid` before falling back to `/files/:sid/:fid`. Both have been there since #13424, but only the second is mounted by any released codeapi — the first gained DELETE in LibreChat-AI/code-interpreter#85 — so every deletion paid a guaranteed 404 and a wasted round trip, and #15511 read that 404 as the whole bug. Call `/files/:sid/:fid` directly. It is the safe direction to collapse toward: codeapi has mounted it since its first release, so this works against older deployments as well as post-#85 ones, whereas keeping the other path would not. Collapsing the loop tightens two behaviours that only existed to serve it. A 405 now surfaces instead of being swallowed on the way to a second attempt; there is no second route to try, and a service that refuses the method should say so. A 404 is still treated as "already gone" — that is the only thing it can now mean — but it is logged rather than passed over in silence, because a 404 caused by a misconfigured base URL looks identical and this branch drops the file's metadata record either way. * 🔒 fix: Settle sweep retry state from the write, not the read Two findings from the review of 0e52924. - `recordFailure` derived the attempt number by adding one to the count the batch had queried. Two nodes sweeping the same file read the same value, so both believed themselves to be the same attempt: each `$inc` landed, the stored count crossed `FILE_RETENTION_SWEEP_MAX_ATTEMPTS`, and neither caller ever saw the threshold. The file drops out of the query — the starvation guard still holds — but the give-up is never reported, and that log line is the operator's only notice, and carries the instruction for resuming. Return the count from the increment itself so every caller gets a distinct attempt number and exactly one observes the cap. The same staleness shortened the backoff, so `deferExpiredFile` now writes with `$max`: a deferral can only move later, and a node that computed a shorter delay cannot pull the file forward past one another node already committed. - The backoff doubled from a hard-coded hour while claiming to start from one sweep interval. At the default they coincide; away from it the schedule stops meaning anything — on a six-hour sweep the first three attempts all land on consecutive passes, and on a five-minute one the first retry skips twelve. Derive the base from `FILE_RETENTION_SWEEP_INTERVAL_MS`, floored at a minute so a pathologically short interval cannot spend the whole give-up budget on a transient outage. * 🧯 fix: Keep the give-up notice and the retry budget honest Four findings from the review of ee1bac5. - The give-up was reported after the deferral write, inside the same catch. Once the increment lands the counter is durable, so `getExpiredFiles` already excludes the file; a deferral that then failed left only the generic recording error and dropped the one line naming the file and saying how to resume. Report it as soon as the increment returns, and let the deferral fail on its own. - Retry deadlines were measured from `Date.now()` after the deletion I/O, but `startExpiredFileSweep` arms its interval before the sweep runs. A one-interval delay therefore expired just *after* the next scheduled pass, which skipped the file and pushed its first retry out by a whole extra interval — and the same drift applied to every delay that is an exact multiple. Anchor deadlines to the sweep's start instead. - The threshold used `>=`, so once two nodes pushed a file past the cap every one of them past it logged the give-up: one error per replica per exhausted file rather than one actionable notice. Only the attempt that lands exactly on the cap reports it. - Retry state outlived the content it described. `processCodeOutput` reuses a record for a repeated `(filename, conversationId)` — new bytes, new storage key, and a fresh `expiredAt` from `getRetentionExpiry` — while `createFile` and `updateFile` set only supplied fields. A record carried to the cap by its previous content stayed excluded from the sweep forever, stranding the new object exactly as this PR set out to prevent; a partially failed one started the new object's budget already spent. Both write paths now clear the fields: the budget belongs to the storage a record currently points at. The retry-state fixtures in `file.spec.ts` were seeded through `createFile`, which now clears them, so they drive the real methods the sweep uses. * 🎯 fix: Clear the retry budget only when a retention lifecycle starts Both findings from the review of 8d26f9b, and both are consequences of that commit's reset rather than of the original change. - The reset was unconditional on every `createFile`/`updateFile`, on the reasoning that those paths write content. Two of them do not. `prepareImages{Local,Azure,Firebase}` call `updateFile({ file_id })` with nothing but the id — a TTL touch — every time an existing image is encoded for another chat, and the deferred preview uses the same method to transition `status`. Either handed a stranded record a fresh set of attempts and another give-up notice, on repeat, defeating the cap for the files most likely to be stranded. Gate it on the write actually setting `expiredAt`. The budget belongs to a retention lifecycle: that is the write which starts a new one, it is what `processCodeOutput` supplies when it repurposes a record, and a record with no retention deadline is never swept, so the fields are inert there anyway. - Both retry writes bumped `updatedAt`. `processCodeOutput` falls back to `updatedAt` as the writer-order stamp for records that predate `metadata.sourceDispatchedAt`, so a failed sweep landing mid-harvest read as a newer content writer and the harvest dropped its attachment. Mark them `timestamps: false`, for the reason `claimCodeFile` already does: bookkeeping is not a content write. *🅿️ refactor: Park exhausted files instead of excluding them Three review rounds in a row found defects in how the give-up cap interacts with record reuse, each in the fix for the last. That is a design error, not a bug list: a permanent exclusion has to be bound precisely to the content lifecycle it was recorded against, and File records outlive their content. `processCodeOutput` repurposes a row for a repeated `(filename, conversationId)`, `createFile`/`updateFile` set only supplied fields, and `getRetentionExpiry` returns `{}` on a lookup failure so the row inherits its old deadline — three separate ways for bookkeeping to survive into a lifecycle it does not describe, each needing its own guard, and the two retry writes needing to be lifecycle-conditional on top. Remove the category instead. `deletionRetryAt` becomes the sweep's only hold, and reaching `FILE_RETENTION_SWEEP_MAX_ATTEMPTS` parks the file for a month rather than excluding it. The bound on the batch is the same — a stranded file costs one slot a month instead of one an hour — but a deadline that outlives its content can only delay the next object, never lose it, so nothing outside the sweep has to reason about this state at all. That deletes more than it adds: - `createFile` and `updateFile` go back to their original form. No reset, so no question of which writes install content, and `prepareImages*` and the deferred preview stop mattering here. - `getExpiredFiles` loses `maxAttempts` and its `$and`; eligibility is one `$or` on the deadline. - The interleaving race between the increment and the deferral degrades from a stranded object to a delayed one. Working through a large backlog is throughput-bound either way: every attempt costs a slot in the bounded batch, so N stranded files need N × `FILE_RETENTION_SWEEP_MAX_ATTEMPTS` passes to settle. Lower that limit when recovering a deployment that has accumulated many.
* 📮 fix: Consume an Invite Only Once the Account Exists `checkInviteUser` deleted the invite token and then called `next()`, but everything that can still reject a registration runs after it: the schema, the allowed-domain check, and the email-already-in-use check. A mistyped password confirmation therefore destroyed the invite — the invitee corrected it, resubmitted, and got "Invalid invite token" with no way back short of an admin re-inviting them. The deletion moves to `registrationController`, after `registerUser` reports success. That report needed a new signal. `registerUser` returns the same 200 and the same generic message whether it created an account or found the email already in use — deliberately, so the response cannot be used to enumerate accounts — so the status alone cannot say whether an account exists. It now also returns `userCreated` on the creation path, which the controller reads and never forwards; the response body is unchanged. A failed deletion is logged rather than surfaced. By that point the account exists, and leaving a usable invite behind is recoverable in a way that telling the user their registration failed is not. Fixes #15541 * ♻️ test: Fold the Invite Tests Into the Existing AuthController Spec From Copilot's review. The new spec reached for `jest.requireActual` on `@librechat/data-schemas`, `~/server/services/AuthService` and `~/models`, which pulls real implementations into a unit test — `~/models` builds the data-schemas methods against mongoose — and diverges from `AuthController.spec.js`, which stubs each module outright. Rather than restate that harness with narrower stubs, the tests move into the spec that already has it. `deleteTokens` joins its `~/models` mock, which also keeps that shared mock in step with the controller's imports: a stale mock there hands the controller an undefined function, and the omission only surfaces when some later test happens to exercise the path.
* 💫 style: Align the Phase Summary Rail and Simplify its Fold A phase summary was the only row in a transcript with no icon rail, so its text sat at 13px while the tool rows it stood for sat at 24px and the rows it swallowed moved out to 37px behind the card's `px-3` — three left edges in one block, with every folded row stepping 13px sideways as the box materialized. - The header takes the same 16px glyph slot every tool row has (`Check`, or `TriangleAlert` when the phase failed), which lands its text on the one rail. - The card chrome is gone: border, background, radius, body padding and divider. The summary is a row among rows, so nothing moves horizontally when it forms, and the entrance is the two grid rows trading places rather than four properties resolving at once. - The label is a ticker. A synthesized card re-titles itself every time it absorbs another finished block, and swapping that text instantly is what made the absorbed row look like it simply vanished; the retired summary now rises out of the clipped row while the new one comes up from below. - The label uses `tool-status-text`, so a summary scales with the reader's font-size setting like the rows around it instead of sitting at a fixed 14px. `ToolCallGroup`'s category glyph was a fourth rail at 28px (`h-5 w-5`); it now matches `ToolIcon` at `size-4`. `ease-[cubic-bezier(0.16,1,0.3,1)]` emitted nothing: `tailwindcss-animate` registers its own `ease` utility for `animation-timing-function` alongside Tailwind's `transition-timing-function` one, an arbitrary value matches both, and Tailwind resolves that ambiguity by dropping the class. The curve had never reached the chrome — the two-easings problem the comment warns about, caused by its own fix. It is written as an arbitrary property now, and it was the only such usage in the repo. * 🩹 fix: Settle the Phase Label in One Commit and Drop a Duplicate Media Query Review follow-ups on the phase ticker. - `useSmoothStreaming` already resolves to `smoothStreaming && !reducedMotion`; subscribing to the same media query again installed one `matchMedia` listener per phase card without changing the answer. - The label copied `text` into state from a passive effect, so a swap with no animation could paint the previous summary for a frame while the button's `aria-label` already carried the new one. It is adjusted during render now, which settles both paths in the same commit. - The incoming line kept its `animate-in` class only while the retired line existed, so clearing that line on its `animationend` could strip the class from a slide still in flight. The class now rides its own flag. - The test's label factory no longer needs `as unknown as`: every field on the ACTIVITY_LABEL part but `type` is optional, and the double cast was only there because a computed key widens to `string`. * ♿ fix: State the Phase Header's Inset Focus Ring Locally The header sits inside a permanent `overflow-hidden` wrapper — the 0fr/1fr grid needs it — so its focus ring has to be inset or it is drawn outside the border box and clipped away. That holds today only because the shared ghost variant supplies `ring-inset`; a change in `packages/client` could remove the indicator from here with no signal. It is declared on the element now, with a test that asserts both the clip and the inset ring.
* feat: Show BYOM worker readiness * test: Exercise approved BYOM commands end to end * test: Wait for BYOM worker readiness
* feat(insights): add agent-scoped access * fix(insights): exclude unattributed assistant messages * fix(sharing): key role insight toggles by principal * fix(sharing): preserve insight grant snapshot * fix(permissions): preserve insight bits atomically * fix(permissions): guard insight audit rollback * fix(insights): keep admin grant controls accessible * fix(permissions): reconcile duplicate principal updates * fix(insights): show automatic admin access * fix(permissions): harden insights access updates * fix(insights): simplify authorization and preserve client state
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )