Conversation
- Introduced a new StatsPanel to display user statistics, including conversation and message counts. - Implemented a Datepicker component for selecting date ranges to filter statistics. - Updated AdminLayout to include the new StatsPanel and integrated it with routing. - Enhanced API to support fetching user statistics based on optional date filters.
🚨 Unused i18next Keys DetectedThe following translation keys are defined in
|
…12673) * fix: endpoint token config not using shared cache in same process (initializing clients) * refactor: Update default max context tokens for agent initialization - Introduced a constant `DEFAULT_MAX_CONTEXT_TOKENS` set to 32000. - Updated the `initializeAgent` function to use this constant instead of hardcoded values for maximum context tokens, improving maintainability and clarity. * refactor: shared caching mechanism for token configuration - Introduced a memoized in-memory cache for Keyv instances to ensure shared access across the same namespace, improving cache efficiency. - Updated the `standardCache` function to utilize the new in-memory cache for the TOKEN_CONFIG namespace. - Refactored the `initializeCustom` function to use the `tokenConfigCache` for better cache management. - Removed redundant tokenCache parameter from `fetchModels` to streamline the function signature. * fix: match TOKEN_CONFIG TTL and add memoization tests Pass Time.THIRTY_MINUTES to tokenConfigCache() to match the TTL used by getLogStores.js, preventing load-order-dependent expiry behavior. Add 7 automated tests covering in-memory memoization: referential identity, cross-call-site data sharing, namespace isolation, first-caller TTL semantics, fallbackStore bypass, and tokenConfigCache parity with direct standardCache access. * fix: export DEFAULT_MAX_CONTEXT_TOKENS, address review nits - Export the constant so tests (and future consumers) reference it directly instead of hardcoding the numeric value. - Add independent TTL assertion for tokenConfigCache (R-1 nit). - Add tokenConfigCache mock to custom/initialize.spec.ts.
…#12672) * fix: add docker system prune before image pull to prevent disk exhaustion The 60GB droplet filled up after ~40 deploys because each docker compose pull leaves the previous image's layers as dangling/unused. The gitnexus image is ~700MB, so ~40 stale copies ≈ 28GB of dead layers. Combined with indexes, OS, and Docker's build cache, the disk hits 100% and the next pull fails with 'no space left on device'. Add a docker system prune -af --volumes BEFORE pulling the new image on every deploy. This removes stopped containers, unused networks, all images not referenced by a running container, and build cache. Running containers are never touched. Typically frees 1-2GB per deploy (the previous image's layers). Also add a hard 2GB free-space guard after prune so the deploy fails with a clear error instead of letting docker pull attempt a 700MB extract onto a near-full disk. * fix: cap PR indexes at 3 + delete-before-sync for 10GB disk The 10GB droplet has ~2GB free. Each index is ~130MB, so 7 PR indexes (~900MB) plus main+dev (~260MB) plus the ~700MB Docker image leaves almost nothing for image pulls. The deploy failed with 'no space left on device' during docker compose pull. Three changes: 1. Cap PR indexes at MAX_PR_INDEXES=3. The resolve step now sorts PR artifacts by created_at descending and only keeps the 3 most recent. Older PR indexes are logged as evicted and their droplet folders get cleaned by the prune step. 2. Prune BEFORE sync (was after). Freeing disk space from evicted indexes before rsyncing new data is critical on a tight disk. The old order (sync then prune) could briefly hold both old evicted indexes and newly-uploaded ones simultaneously. 3. Delete-before-sync for every index, including main/dev. Instead of rsync --delete (which transfers new files then removes extras), rm -rf the target folder before rsync so the disk never holds both old and new copies of the same index (~260MB saved per index). Main/dev are only deleted when a fresh artifact is about to replace them — never evicted between deploys. Budget on 10GB disk: OS + Docker engine: ~4.0 GB Docker image (running): ~0.7 GB main + dev indexes: ~0.26 GB 3 PR indexes: ~0.39 GB Docker prune headroom: ~0.7 GB (for image pull) Free: ~3.9 GB * refine: restrict automatic PR indexing to danny-avila authored PRs With 200+ open PRs and a 10GB disk capped at 3 served PR indexes, auto-indexing every contributor PR burns CI minutes for artifacts that will mostly be evicted before anyone queries them. Narrow the pull_request auto-trigger to PRs authored by danny-avila only. Other contributors' PRs can still be indexed on demand via /gitnexus index (contributor-gated comment command) or manual workflow_dispatch — both arrive as workflow_dispatch events and bypass the pull_request filter entirely. * fix: drop --volumes from docker system prune to preserve Caddy TLS state The deploy workflow explicitly handles a caddy-not-running state later in the same step. If Caddy is stopped when the prune runs, --volumes deletes the caddy-data and caddy-config volumes (TLS certs + ACME account keys), forcing a Let's Encrypt re-issuance on next start. LE rate-limits to 5 certs per domain per week, so repeated wipes could brick HTTPS for days. docker system prune -af (without --volumes) still removes stopped containers, unused networks, all dangling/unreferenced images, and build cache — which is where the disk savings come from. Named volumes are left untouched. * fix: rsync-then-swap instead of delete-before-sync The delete-before-sync pattern removed the live index BEFORE rsync ran. If rsync failed (SSH timeout, disk pressure, network error), the index was already gone — production served nothing for that repo until a later deploy succeeded. Replace with rsync-then-swap: upload to a .new temp directory, and only rm + mv into place after rsync succeeds. On rsync failure, the .new temp is cleaned up and the old index stays live. The cost is ~130MB of extra disk while both old and new coexist, but the prune step runs first and frees evicted PR indexes, so this fits comfortably on the 10GB disk. * fix: fail deploy on main/dev rsync failure, soft-fail PRs only The rsync-then-swap pattern downgraded ALL failures to a warning, so the deploy continued even when LibreChat or LibreChat-dev failed to sync. The job would pull the new image, restart the container, and report success while serving stale or missing core indexes. Split by criticality: main/dev rsync failures now exit 1 (aborting the deploy before the container restart). PR index failures remain soft-fail with a warning — a missing PR index is inconvenient but shouldn't take the whole server down.
…-AI#12674) * fix: normalize audio MIME types in STT format validation Use getFileExtensionFromMime() to normalize non-standard MIME types (e.g. audio/x-m4a, audio/x-wav, audio/x-flac) before checking against the accepted formats list in azureOpenAIProvider. This is the same class of bug as LibreChat-AI#12608 (text/x-markdown), but for STT audio validation. Only audio/ and video/ MIME prefixes are normalized to prevent non-audio types from matching via the webm default fallback. Export getFileExtensionFromMime for testability. Fixes LibreChat-AI#12632 * fix: reject unknown audio subtypes in STT format validation Use MIME_TO_EXTENSION_MAP for normalization instead of getFileExtensionFromMime() which falls back to 'webm' for unrecognized types. Gate raw subtype matching on audio/video prefix to prevent non-audio types (e.g. text/webm) from passing validation. Resolves Codex review comment about unknown subtypes silently passing. --------- Co-authored-by: Tobias Jonas <t.jonas@innfactory.de>
…LibreChat-AI#12386) * fix: handle `usage_metadata` in title transaction for Gemini models Gemini models return token usage via `usage_metadata` instead of `usage` or `tokenUsage`. The `collectedUsage` mapping in `titleConvo` only handled the latter two, causing title generation transactions to be silently skipped for Gemini. Adds an `else if (item.usage_metadata)` branch to extract `input_tokens`/`output_tokens`. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove trailing whitespace in usage_metadata handler Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…12676) * style: Update padding in ActionsPanel, ModelPanel, and AdvancedPanel for improved layout * Adjusted padding in ActionsPanel, ModelPanel, and AdvancedPanel components to enhance visual consistency and layout. * Changed `py-4` to `pt-2` in the main container of each panel to reduce vertical spacing and improve overall design aesthetics. * style: Update text size in MCPTool component for improved accessibility * Changed text size in the MCPTool component to `text-sm` for better readability and consistency across the UI. * This adjustment enhances the user experience by ensuring that text is appropriately sized for various display settings. * style: Enhance layout and accessibility in ActionsInput, ActionsPanel, and AgentPanel components * Updated the layout in ActionsInput to improve flex properties and ensure better responsiveness. * Refined the structure of ActionsPanel for a more consistent visual hierarchy and added accessibility features. * Adjusted the AgentPanel form layout for improved usability and streamlined component integration. * Changed text size in Dropdown component to `text-sm` for better readability across the UI. * style: Refactor layout in ActionsInput component for improved responsiveness * Updated the layout of the ActionsInput component to enhance flex properties and ensure better responsiveness. * Adjusted the textarea styling for improved usability and consistency in design. * Removed commented-out code related to example functionality to clean up the component structure. * refactor: Simplify single line code detection in MarkdownComponents * Introduced a new utility function `isSingleLineCode` to streamline the logic for determining if code is a single line. * Updated references in the `MarkdownCode` and `MarkdownCodeNoExecution` components to use the new utility function for improved readability and maintainability. * Enhanced the `processChildren` function in the Markdown editor to handle non-code elements more effectively.
* refactor: Improve UX for Command Popovers * Added loading state handling in Mention and PromptsCommand components to display a spinner when data is being fetched. * Refactored onFocus logic to clear the textarea and set the search value based on command character input. * Introduced a new `isLoading` state in the useMentions hook to manage loading indicators across multiple data queries. * Added unit tests for the useHandleKeyUp hook to ensure command triggering works correctly under various conditions. * ci: useHandleKeyUp tests for command navigation * Added tests to ensure that the command popovers do not trigger when the cursor is mid-text after pressing ArrowLeft or Delete. * Updated the shouldTriggerCommand function to refine the conditions under which commands are triggered based on cursor position. * Improved agent query handling in useMentions hook for better performance and clarity. * refactor: Optimize Mention and PromptsCommand Components * Refactored Mention and PromptsCommand components to utilize Recoil state for popover visibility, improving state management and reducing prop drilling. * Simplified onFocus logic to enhance user experience when interacting with command inputs. * Added unit tests for useHandleKeyUp to ensure proper command handling and popover visibility based on user input. * Improved performance by memoizing popover state and reducing unnecessary re-renders. * fix: Address review findings for command popover refactor - Fix endpointType regression: add effectiveEndpointByIndex selector that returns endpointType ?? endpoint, matching the original ChatForm guard for custom endpoints proxying assistants - Extract duplicated initInputRef callback into shared useInitPopoverInput hook, used by both Mention and PromptsCommand - Add navigation keys (ArrowLeft, ArrowRight, ArrowDown, Home, End, Delete) to invalidKeys to prevent false popover triggers - Add endpoint gating tests for assistants/azureAssistants blocking the + command - Remove unused _index param from MentionContent
* 🦉 feat: Claude Opus 4.7 Model Support
- Add `claude-opus-4-7` to shared Anthropic models and `anthropic.claude-opus-4-7` to Bedrock models
- Register 1M context window and 128K max output in anthropic token maps
- Add token pricing ($5/$25), cache rates (6.25/0.5), and premium tier ($10/$37.50 above 200K) in tx.ts
- Update `.env.example` with Opus 4.7 IDs in `ANTHROPIC_MODELS` and `BEDROCK_AWS_MODELS` examples
- Add parallel Opus 4.7 test cases for token/cache/premium rates, context length, max output, name-variation matching, and 1M-context qualification
* feat: Add `xhigh` Effort Level for Opus 4.7
- Add `xhigh` variant to `AnthropicEffort` enum between `high` and `max`
- Expose `xhigh` in `anthropicSettings.effort.options` and the UI slider `enumMappings`
- Reuse existing `com_ui_xhigh` translation key
* test: Cover `xhigh` Effort and Exact Opus 4.7 Premium Rates
- Assert `xhigh` position (between high and max), inclusion in
`anthropicSettings.effort.options`, zod acceptance, and rejection of
unknown values in schemas.spec.ts
- Verify bedrockInputParser emits `output_config: { effort: 'xhigh' }`
for adaptive `anthropic.claude-opus-4-7`
- Verify getLLMConfig sets adaptive thinking and `output_config.effort =
'xhigh'` for `claude-opus-4-7`
- Pin Opus 4.7 premium pricing to exact threshold/prompt/completion
values (200000 / 10 / 37.5) so silent rate drift fails the test
* 🫧 fix: Restore Claude Opus 4.7 Reasoning Visibility
Claude Opus 4.7 omits `thinking` content from Messages API responses by
default — empty thinking blocks still stream, but the `thinking` field is
blank unless the caller passes `display: "summarized"` in the adaptive
thinking config. This silenced the LibreChat "Thoughts" UI for Anthropic
(and Anthropic-on-Bedrock) adaptive models.
- Extend `ThinkingConfigAdaptive` in `packages/api/src/types/anthropic.ts`
with an optional `display: 'summarized' | 'omitted'` field
- Emit `{ type: 'adaptive', display: 'summarized' }` from
`configureReasoning` in `packages/api/src/endpoints/anthropic/helpers.ts`
- Emit `{ type: 'adaptive', display: 'summarized' }` from
`bedrockInputParser` in `packages/data-provider/src/bedrock.ts` and
update the local `ThinkingConfig` union
- Update existing adaptive-thinking assertions to include the new field
- Add dedicated tests asserting `display: 'summarized'` flows through
both the Anthropic endpoint and the Bedrock parser
See https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7#thinking-content-omitted-by-default
* refactor: Gate `display: summarized` on Opus 4.7+
Narrow the reasoning-visibility opt-in to the models that actually omit
thinking content by default, instead of applying it to every adaptive
model. Pre-Opus-4.7 adaptive models (Opus 4.6, Sonnet 4.6) already return
summaries, so sending the field is unnecessary noise.
- Add `omitsThinkingByDefault(model)` in `packages/data-provider/src/bedrock.ts`
that returns true only for Opus 4.7+ (including future majors like Opus 5+)
- Bedrock parser now only attaches `display: 'summarized'` when the helper
matches, keeping the adaptive object unchanged for older models
- Anthropic endpoint `configureReasoning` uses the same helper so its emit
path matches the Bedrock one
- Tests: replace the blanket `display: 'summarized'` assertions with
model-specific ones (Opus 4.7 gets it, Opus 4.6 / Sonnet 4.6 do not),
add a dedicated `omitsThinkingByDefault` suite covering naming variants
and future versions
* feat: Configurable Thought Visibility for Anthropic Adaptive Models
Expose the Anthropic `thinking.display` API field as a user-facing
parameter so users can override the `auto` default (which stays as the
Opus-4.7+ opt-in added earlier in this PR). Also fixes the CI type error
by widening the adaptive thinking type assignment via a resolver helper
that returns a properly-typed object.
- Add `ThinkingDisplay` enum (`auto` | `summarized` | `omitted`) and
matching zod schema in `packages/data-provider/src/schemas.ts`
- Add `thinkingDisplay` to `tConversationSchema`, `anthropicSettings`, and
the pick lists for Bedrock input/parser + Anthropic agent params
- Add `resolveThinkingDisplay(model, explicit)` helper in
`packages/data-provider/src/bedrock.ts` that returns the wire value or
undefined (auto → model default, explicit → always honored)
- `bedrockInputParser` now reads `thinkingDisplay` from input and emits
`display` only when the resolver returns a value; strips the field
on non-adaptive-model branches so it does not leak
- `configureReasoning` in the Anthropic endpoint threads
`thinkingDisplay` through, uses the resolver, and casts the adaptive
config to `AnthropicClientOptions['thinking']` so the widened shape
compiles against the stale installed SDK types
- Add UI slider for `thinkingDisplay` in `parameterSettings.ts` next to
`effort`, with three-position `com_ui_auto` / `com_ui_summarized` /
`com_ui_omitted` labels
- Add translation keys `com_endpoint_anthropic_thinking_display`,
`com_endpoint_anthropic_thinking_display_desc`, `com_ui_summarized`,
`com_ui_omitted`
- Add tests: `resolveThinkingDisplay` suite (5 cases covering auto /
explicit / unknown input), parser round-trip tests for all three
modes on Opus 4.6 and Opus 4.7, Anthropic endpoint tests for explicit
summarized/omitted overrides
* fix: Drop `thinkingDisplay` When Adaptive Thinking Is Disabled
If a user turns adaptive thinking off but had previously selected a
`thinkingDisplay` value, the stale field was left in `additionalFields`
and ended up merged into the Bedrock request's
`additionalModelRequestFields`. That leaks a non-Bedrock key into the
payload and can round-trip back into `llmConfig`.
- Delete `additionalFields.thinkingDisplay` alongside `thinking` and
`thinkingBudget` in the `thinking === false` branch of
`bedrockInputParser`
- Add a regression test asserting `thinking`, `thinkingBudget`, and
`thinkingDisplay` are all absent when adaptive thinking is disabled on
an Opus 4.7 request
Reported by chatgpt-codex-connector on PR LibreChat-AI#12701.
* refactor: Consolidate `ThinkingDisplay` Types and Preserve Persisted Display
Address review findings on PR LibreChat-AI#12701:
- [Codex P2] `bedrockInputSchema.transform` now extracts
`thinking.display` from persisted `additionalModelRequestFields` back
into the top-level `thinkingDisplay` field so explicit `'omitted'`
round-trips through storage instead of being silently reverted to
`'summarized'` on the next parse.
- [Codex P2] `getLLMConfig` in the Anthropic endpoint now reads
`.display` from a persisted `thinking` object (agents store the full
Anthropic shape) and uses it as the fallback for `thinkingDisplay`
when no top-level override is present.
- [Audit #2] Collapse the three parallel wire-value types into a single
`ThinkingDisplayWireValue = Exclude<ThinkingDisplay, 'auto'>` exported
from `schemas.ts`; remove the duplicate `ThinkingDisplay` alias in
`packages/api/src/types/anthropic.ts` (which collided with the enum
name) and the `ThinkingDisplayValue` alias in `bedrock.ts`.
- [Audit #3] Add `thinkingDisplay` to the `TEndpointOption` pick list
next to `effort`.
- [Audit #4] Add a TODO comment next to the `as
AnthropicClientOptions['thinking']` cast explaining the stale
`@librechat/agents` SDK types that require it.
- Add tests: four round-trip cases asserting `bedrockInputSchema`
recovers `display` from persisted AMRF (Opus 4.7 omitted, pre-4.7
summarized, unknown-value ignore, explicit top-level wins), and two
`getLLMConfig` cases asserting the Anthropic endpoint preserves and
overrides persisted `thinking.display`.
* fix: Preserve Persisted `thinking.display` in bedrockInputParser
The parser constructed a fresh adaptive thinking config without looking at
any `display` already embedded in the incoming
`additionalModelRequestFields.thinking`. On round-trip through
`initializeBedrock`, a persisted user choice of `'omitted'` on Opus 4.7+
was silently reverted to `'summarized'` by the auto fallback.
- Extract `extractPersistedDisplay` helper and reuse it in both the
schema transform (form-state round-trip) and the parser (wire-request
round-trip)
- `bedrockInputParser` now feeds the persisted display as the resolver's
explicit value when no top-level `thinkingDisplay` override is set
- Add regression tests: parser preserves `display: 'omitted'` for
persisted Opus 4.7 AMRF, and top-level `thinkingDisplay` still wins
over persisted AMRF display
Reported by chatgpt-codex-connector (P1) on PR LibreChat-AI#12701.
…I#12710) * chore: Update package-lock.json with new dependencies and version upgrades - Added new dependencies for @langchain/anthropic and @langchain/core, including @anthropic-ai/sdk and fast-xml-parser. - Updated existing dependencies for @librechat/agents, @opentelemetry/api-logs, @opentelemetry/core, and related packages to their latest versions. - Enhanced integrity checks and licensing information for new and updated packages. * chore: Update @librechat/agents dependency to version 3.1.66 in package.json and package-lock.json - Bumped the version of @librechat/agents from 3.1.65 to 3.1.66 across multiple package.json files to ensure consistency and access to the latest features and fixes. * chore: Update dompurify and fast-xml-parser dependencies to version 3.4.0 and 5.6.0 respectively - Bumped the version of dompurify across multiple package.json files to ensure consistency and access to the latest features and security fixes. - Updated fast-xml-parser to the latest version in relevant package.json files for improved functionality. * chore: Update @librechat/agents dependency to version 3.1.67 in package.json and package-lock.json - Bumped the version of @librechat/agents from 3.1.66 to 3.1.67 across multiple package.json files to ensure consistency and access to the latest features and fixes.
…breChat-AI#12707) * fix(agents): normalize handoff prompt and parameter values to ensure default instructions * fix(agents): address lint errors and codex review comments
…ility (LibreChat-AI#12736) * 🐛 fix: replace `$bitsAllSet` ACL queries for Cosmos DB compatibility Azure Cosmos DB for MongoDB API does not implement the `$bitsAllSet` query operator, so every permission check against Cosmos DB threw. The five read paths in `aclEntry.ts` (`hasPermission`, `findAccessibleResources`, `findPublicResourceIds`, and two sites in `getSoleOwnedResourceIds`) now fetch candidate entries and apply the bitwise mask in application code. This matches the existing FerretDB-compatible pattern. Fixes LibreChat-AI#12729. * 🐛 fix: delegate `findPubliclyAccessibleResources` to fixed DB method `AccessControlService.findPubliclyAccessibleResources` inlined the same `$bitsAllSet` query as the data-schemas layer, which fails on Azure Cosmos DB for MongoDB. Delegate to `_dbMethods.findPublicResourceIds` so a single implementation carries the Cosmos-compatible bitwise logic. Refs LibreChat-AI#12729. * 🐛 fix: move `$bitsAllSet` filter out of remote-agent aggregation `enrichRemoteAgentPrincipals` used `$bitsAllSet` inside an aggregation `$match`, which Azure Cosmos DB for MongoDB does not implement. Project `permBits` through the pipeline and filter for `PermissionBits.SHARE` in application code. The extra documents fetched are bounded by ACL entries on a single agent resource, so the cost is negligible. Refs LibreChat-AI#12729. * 🧪 test: rename misleading public-dedup test and add real dedup coverage The test previously named "returns deduplicated IDs even if the public principal has multiple entries" only set up a single ACL entry, so it did not actually exercise deduplication. Split into two tests: one for the happy path (single entry with required bits), and one that bypasses `grantPermission`'s upsert via `AclEntry.create` to confirm the application-layer dedup Map handles genuine duplicates. Refs LibreChat-AI#12729. * 🧪 test: cover SHARE-bit filter in `enrichRemoteAgentPrincipals` The `$bitsAllSet` match stage in `enrichRemoteAgentPrincipals` previously guaranteed every aggregation row had SHARE; the Cosmos DB fix moved that check into a JS `continue` branch with no direct coverage. Add a dependency-injected unit test that stubs the aggregation with mixed SHARE / non-SHARE / zero-bit rows and asserts only SHARE holders are enriched and queued for backfill. Also includes a regression guard that the `$match` pipeline stage no longer contains a `permBits` filter. Refs LibreChat-AI#12729. * ♻️ refactor: extract `filterByBitsAndDedup` helper for ACL reads `findAccessibleResources` and `findPublicResourceIds` each inlined the same bitmask-filter + `Map`-based dedup loop. Lift it into a private `filterByBitsAndDedup(entries, requiredBits)` helper so the Cosmos-DB compatible pattern lives in one place. Pure rename/extract — no behavior change. Refs LibreChat-AI#12729. * 📝 docs: fix stale `\$bitsAllSet` references in FerretDB spec The describe block and header comment in the FerretDB parity spec still referenced `\$bitsAllSet queries` after the Cosmos DB compatibility fix moved the bit mask into application code. Update the title to \"Bitwise permission queries\" and rewrite the header comment to describe the application-layer behavior being validated. Refs LibreChat-AI#12729. * ⚡ perf: push permission-mask filter back to the database via `$in` The original fix for LibreChat-AI#12729 moved `$bitsAllSet` filtering into application code, which meant every ACL read fetched the full set of rows for a principal/resource and filtered in JS. For tenants with large ACL collections this inflates wire transfer and heap. Replace the JS filter with `permBits: { $in: permissionBitSupersets(X) }`. For the 4-bit `PermissionBits` enum the `$in` list is at most 16 values (8 for a single-bit mask like SHARE). `$in` is indexable and supported by Azure Cosmos DB for MongoDB, so the filter runs on the server again — restoring `.distinct('resourceId')` and `findOne()` semantics. `permissionBitSupersets(requiredBits)` is memoized and exported from `@librechat/data-schemas`. Callers restored: - `hasPermission`: back to `findOne` short-circuit - `findAccessibleResources` / `findPublicResourceIds`: back to `.distinct()` - `getSoleOwnedResourceIds`: back to the `$match` + `$group` aggregation - `enrichRemoteAgentPrincipals`: bit filter back in `$match`, JS `continue` removed Refs LibreChat-AI#12729. * 🧪 test: add `\$bitsAllSet` vs `\$in` parity + perf spec Introduces `aclEntry.parity.spec.ts` — a side-by-side spec that runs the legacy `\$bitsAllSet` query and the current `\$in`-based query against the same `mongodb-memory-server` fixture and asserts identical output sets for every affected method (`hasPermission`, `findAccessibleResources`, `findPublicResourceIds`, `getSoleOwnedResourceIds`) across all 7 meaningful permBits combinations. Also logs median wall-clock time for the two query paths over 20 runs on an 800-entry fixture, with a loose 3x guard against catastrophic regressions. Initial local numbers: 1.05 ms vs 1.07 ms (findAccessibleResources), 1.10 ms vs 1.05 ms (findPublicResourceIds). Refs LibreChat-AI#12729. * 🔒 hardening: freeze `permissionBitSupersets` cache + enum-shape guard Two defensive changes from the comprehensive audit: * Cached superset arrays are now `Object.freeze`d and the return type is `readonly number[]`. Previously the cached arrays were returned by mutable reference, so a caller that mutated the result would silently corrupt the process-wide cache for every subsequent permission check. `Object.freeze` turns that into a loud `TypeError` at the mutation site. All existing call sites pass the result directly to Mongoose's `$in`, which does not mutate. * Added a module-load guard `if (MAX_PERM_BITS === 0) throw`. If `PermissionBits` is ever refactored to a `const` object or string enum, `Object.values(...).filter(isNumber)` would return `[]` and `MAX_PERM_BITS` would silently become 0, making every query match no rows and breaking every permission check. The guard fails loudly instead. Also collapsed four identical JSDoc lines across `hasPermission`, `findAccessibleResources`, `findPublicResourceIds`, and `getSoleOwnedResourceIds` into a single `{@link permissionBitSupersets}` reference. Refs LibreChat-AI#12729. * 🧪 test: add focused unit tests for `permissionBitSupersets` The helper is the single point of correctness for every ACL read path (every query uses `permBits: { $in: permissionBitSupersets(X) }`), so it warrants direct coverage independent of the higher-level parity and behavior specs. Six cases added: * `requiredBits=0` returns all 16 values * `requiredBits=15` returns `[15]` only * every returned value is a bitwise superset of `requiredBits` * full parity against the `$bitsAllSet` definition for every `required` in 0..15 * memoization: repeat calls return the same frozen reference * frozen result throws `TypeError` on mutation attempts Refs LibreChat-AI#12729. * 🧪 test: tighten parity perf guard and document fixture constants The `expect(currentMs).toBeLessThan(legacyMs * 3 + 50)` form was dominated by the `+ 50` additive term at typical sub-ms query latencies — at legacy=1ms, a 50x regression would still pass. Replace with `Math.max(legacyMs * 5, 50)` so the multiplicative ceiling is intact once the new path climbs out of the fixed noise floor. Also added inline rationale for the `FIXTURE_SIZE = 800` and `PERF_ITERATIONS = 20` constants. Refs LibreChat-AI#12729. * 🧹 chore: remove stale perf-guard comment, hoist rationale to describe Commit a75f122 left a "Sanity check: A 3x multiplier" comment block above the first perf guard, and 21852ac layered a second "Multiplicative-dominant guard" block directly below it. The stale block described the `legacyMs * 3 + 50` formula that no longer exists, so both blocks coexisted and contradicted each other. Delete the stale block from the first test, remove the redundant copy from the second test, and lift the (now-single) rationale to the enclosing `describe('performance')` block. Each test is now one `expect` line — the rationale lives once, at the scope it applies to. Refs LibreChat-AI#12729. * 📝 docs: sharpen MAX_PERM_BITS guard message + test-sort consistency Two NITs from the follow-up audit: * The `MAX_PERM_BITS === 0` guard now names the specific refactors that could cause it (const object / string enum / all-string shape) and gives two concrete remediation paths (rewrite the reducer, or give `permissionBitSupersets` an explicit bit-width parameter). Previously the message just said "update permissionBitSupersets before continuing", which was vague. * The `requiredBits=15` unit test now applies the same `[...result].sort((a, b) => a - b)` normalization as the other tests so the set-equality assertions are uniform. The function happens to return values in ascending order, but the helper's JSDoc does not promise ordering, so sorting before comparing is the correct defensive pattern. Refs LibreChat-AI#12729. * 🔒 fix: enforce `permBits <= MAX_PERM_BITS` at the schema level Codex flagged a real silent regression: `permissionBitSupersets` enumerates `$in` candidates only in `[0, MAX_PERM_BITS]` (currently 15), so any ACL row with bits above the enum — e.g. `permBits = 31` from a future role or a manual DB write — would be excluded from reads that `$bitsAllSet` would have matched. Current write paths only produce values in `[0, 15]` via the `PermissionBits` / `RoleBits` enums, so no live data is affected, but the schema did not prevent out-of-range writes, so the divergence was reachable. Fix: add `min: 0`, `max: MAX_PERM_BITS`, and an `isInteger` validator to the `permBits` field in the AclEntry schema. `MAX_PERM_BITS` is derived from `PermissionBits` the same way `permissionBitSupersets` computes it, so when a new bit is added to the enum both the read-side enumeration and the write-side bound expand together. Tests: four new cases cover the upper bound, over-limit, negative, and non-integer inputs, each asserting `Mongoose.Error.ValidationError`. Plus updated JSDoc on `permissionBitSupersets` to document the invariant that the schema now enforces. Refs LibreChat-AI#12729. * ♻️ refactor: hoist `MAX_PERM_BITS` + enum-shape/ceiling guards to shared util Addresses one MINOR DRY finding and one NIT defensive-guard finding from the latest audit. `MAX_PERM_BITS` was duplicated in `methods/aclEntry.ts` and `schema/aclEntry.ts` with identical computations; they could silently diverge. Move the constant plus both module-load guards to `src/common/permissions.ts`: * `MAX_PERM_BITS === 0` guard — fail loudly if `PermissionBits` is ever refactored to a `const` object or string enum (applied in both sites now, not just methods). * `MAX_PERM_BITS > 255` guard — circuit-breaker for the `$in` enumeration strategy. At 4 bits the list tops out at 16; at 8 bits 256. Beyond that the approach degrades, so fail at module load rather than emit an unusably-large `$in` list. Both the schema and the methods file now import from the single `src/common/permissions.ts`. `readonly number[]` return type and `Object.freeze` cache-value protection from the prior commits are preserved. Refs LibreChat-AI#12729. * 🔒 fix: reject out-of-range `requiredBits` without caching (DoS fix) Codex flagged a real unbounded-cache DoS vector: `api/server/controllers/agents/v1.js` parses `req.query.requiredPermission` via `parseInt` and forwards it unvalidated to `findAccessibleResources`, which eventually calls `permissionBitSupersets(requiredBits)`. Because the old code memoized *every* distinct input in a process-global Map, an attacker could flood the cache with unique integers and grow memory without bound. Fix: when `requiredBits` is not an integer, is negative, or has any bits set above `MAX_PERM_BITS`, return a single shared frozen empty array and do NOT touch the cache. An empty `$in` list correctly matches zero rows (rows cannot satisfy a bit we do not support), so the response is also semantically correct — previously it "worked" only because invalid inputs coincidentally expanded to `[]` too, but at the cost of a cache entry each time. Five new tests exercise the rejection path: upper-bound overflow, mixed in-range+out-of-range bits, shared-instance identity across rejected inputs, a 2000-unique-value cache-growth probe, and a regression check that legitimate in-range inputs still get memoized normally. Refs LibreChat-AI#12729. * 🧪 test: probe `Map.prototype.set` to prove cache doesn't grow on rejection Strengthens the DoS cache-growth test the review pass flagged: reference identity alone allows a hypothetical regression like `supersetCache.set(requiredBits, EMPTY_SUPERSETS); return EMPTY_SUPERSETS;` to pass while still leaking one entry per attacker request. Add a second test that spies on `Map.prototype.set`, records the global call count before and after a burst of 1500 rejected inputs (500 each of over-MAX integers, negatives, and non-integers), and asserts the delta is zero. Manually verified the test has teeth: injecting the hypothetical regression in the implementation produced exactly 1500 extra `Map.set` calls and the new test failed as expected; reverting restored the clean state. Renames the existing identity-based test to `(reference identity)` so its scope is clear alongside the new `(Map-write probe)` companion. Refs LibreChat-AI#12729.
…AI#12734) * 🐛 fix: Preserve Raw Markdown on `Upload as Text` When `RAG_API_URL` is configured, `.md` uploads were sent to the RAG API `/text` endpoint, which routes Markdown through `UnstructuredMarkdownLoader` and strips formatting (`#`, `**`, lists, blockquotes). Users expect `Upload as Text` to preserve raw content - identical bytes in a `.txt` file round-trip verbatim, while the `.md` came back stripped. Short-circuit the RAG API call for Markdown files (by MIME type or `.md` / `.markdown` extension) and read the file verbatim via `parseTextNative`. Non-Markdown paths are unaffected, and the embedding path (`/embed`) keeps its existing loader so vector search quality is unchanged. * 🐛 fix: normalize markdown MIME and accept `text/md` Addressing review feedback on the `Upload as Text` short-circuit: - Accept `text/md` in the markdown MIME set (LibreChat treats it as a valid markdown type elsewhere, e.g. the artifact-rendering prompt). - Normalize the incoming MIME type (lowercase + strip parameters) before the set lookup so parameterized values like `text/markdown; charset=utf-8` and uppercase `TEXT/MARKDOWN` still short-circuit. Extensionless uploads relying only on the `Content-Type` header would otherwise fall through to the RAG `/text` endpoint and lose their markdown formatting. Extend `text.spec.ts` parametrized cases with `text/md`, parameterized MIME, uppercase, and whitespace-padded variants. * 🧹 chore: Address Code Review Follow-ups on `Upload as Text` fix Addressing comprehensive review feedback: - Debug log now includes filename and MIME type so operators can identify which upload triggered the short-circuit without having to correlate other logs. - Expand markdown extension detection beyond `.md` / `.markdown` to cover `.mdown`, `.mkdn`, `.mkd`, `.mdwn` (case-insensitive regex). - Tighten `normalizeMimeType` parameter type from `string | undefined` to `string` to match the actual Express.Multer.File type. The falsy-check still protects against empty strings at runtime. - Extend parametrized tests with the most common real-world shapes: `text/plain` + `.md` (the MIME most browsers/servers assign), the new rare extensions, and empty MIME + `.md` (pure extension fallback path). - Add a positive assertion that `readFileAsString` was called with the expected arguments on every short-circuit case, so tests fail loudly if the native-parse path ever regresses. * 🧪 test: Cover `.mdwn` regex branch in Markdown short-circuit Every other alternation in `MARKDOWN_EXTENSIONS_RE` has at least one test case (`md`, `markdown`, `mdown`, `mkdn`, `mkd`) but `mdwn` did not, leaving a typo in that branch undetectable.
…-Supported Types (LibreChat-AI#12735) * 🐛 fix: accept documented `summarization.trigger.type` values The Zod schema for `summarization.trigger.type` only accepted `'token_count'`, but: - the documentation lists `token_ratio`, `remaining_tokens`, and `messages_to_refine` as valid - the `@librechat/agents` runtime only evaluates those three types and silently no-ops on anything else The result was a double failure: any user following the docs hit a startup Zod error, and anyone who matched the schema by using `token_count` got a silent no-op at runtime where summarization never fired. Align the schema with the documented, runtime-supported trigger types. Closes LibreChat-AI#12721 * 🧹 fix: bound `token_ratio` trigger value to (0, 1] Per Codex review: the previous schema accepted `value: z.number().positive()` for every trigger type. That meant `trigger: { type: 'token_ratio', value: 80 }` (presumably meant as "80%") passed validation and then silently never fired — because `usedRatio = 1 - remaining/max` is bounded at 1, so `>= 80` is always false. That is exactly the silent-no-op pattern this PR is trying to eliminate. Switch to a discriminated union so each trigger type has its own value constraint: - `token_ratio`: `(0, 1]` — documented as a fraction, so 80 is nonsense - `remaining_tokens`: positive — token counts can be large - `messages_to_refine`: positive — message counts can be > 1 Added tests for the upper-bound rejection and the inclusive upper bound (`value: 1` still accepted as a valid "fire at 100%" extreme). * 🧹 fix: accept `token_ratio: 0` per documented 0.0–1.0 inclusive range Per Codex review: `.positive()` rejected `value: 0`, but the docs describe the `token_ratio` range as `0.0–1.0` (both inclusive). Admins who copy the documented lower bound into their YAML would fail schema validation at startup. Switch `token_ratio` to `.min(0).max(1)`. `0` is a valid (if extreme) setting — the agents SDK's `usedRatio >= 0` check will fire as soon as there is anything to refine, which is a legitimate "always summarize when pruning happens" configuration. `remaining_tokens` and `messages_to_refine` keep `.positive()`: both are counts, and `0` there produces no meaningful behavior (the SDK has an early return for `messagesToRefineCount <= 0`). * 🐛 fix: preserve `token_ratio` trigger when `value: 0` Per Codex review: now that the schema accepts `token_ratio: 0`, `shapeSummarizationConfig` would silently drop it because of a truthy check on `config?.trigger?.value`. The trigger would disappear and the runtime would fall back to "no trigger configured" — which fires on any pruning rather than honoring the explicit ratio. Switch to `typeof value === 'number'`, which preserves `0` while still rejecting `undefined`/`null`. Added a regression test that asserts `{ type: 'token_ratio', value: 0 }` survives the shaping function untouched. * 🧹 fix: reject non-finite trigger values at schema level Per Codex review: `z.number().positive()` still accepts `Infinity` and `NaN` (via YAML `.inf`, `.nan`). Config validation would succeed, but the agents SDK guards every trigger path with `Number.isFinite(...)` and silently returns `false` — summarization never fires while the server starts cleanly. That is the exact schema/runtime split this PR is trying to eliminate. Add `.finite()` to every trigger value. `token_ratio` already had an implicit guard via `.max(1)`, but applying `.finite()` uniformly keeps the intent obvious and catches `NaN` (which `.max(1)` does not). * 🧹 fix: integer counts + targeted token_count migration warning Two findings from the comprehensive review: 1. `remaining_tokens` and `messages_to_refine` are token/message counts and are always integers in the runtime (`Number.isFinite(...)` guards already assume integer semantics). `z.number().positive()` accepted fractional values like `2.5`, which was semantically confusing and would round oddly against the runtime's `>=` / `<=` comparisons. Add `.int()` to both count-based branches; `token_ratio` stays fractional. 2. Anyone upgrading with `trigger.type: 'token_count'` in their YAML got the generic "Invalid summarization config" warning plus a flattened Zod error. Detect that specific case in `loadSummarizationConfig` and emit a migration-friendly message that names the three valid replacements. Export the function so the behavior is unit-testable. Also added a parameterized passthrough test covering `remaining_tokens` and `messages_to_refine` shaping, complementing the existing `token_ratio` coverage. * 🧹 fix: accurate fallback wording + bare-string trigger test Two nits from the follow-up audit: 1. The legacy-`token_count` warning claimed "Summarization will be disabled," but `shapeSummarizationConfig` treats a missing summarization config as self-summarize mode (fires on every pruning event using the agent's own provider/model). "Disabled" would mislead an admin into stopping investigation. Reword to describe the actual fallback and assert the new wording in the spec. 2. Add a regression test for the `trigger: 'bare-string'` YAML case, so the `typeof raw.trigger === 'object'` guard is exercised rather than implied. 3. Swap the en-dash in `(0–1)` for an ASCII hyphen so the log message is safe in every terminal/aggregator regardless of UTF-8 handling. * 🔇 fix: cast `raw.trigger.type` to inspect legacy value past narrowed union CI TS check failed: after the schema tightening, `raw.trigger.type` is narrowed to `"token_ratio" | "remaining_tokens" | "messages_to_refine" | undefined`, so the runtime comparison to `"token_count"` is a TS2367 ("no overlap") error even though that's exactly the comparison we want for the migration guard. Widen just that one access via `as { type?: unknown }` so the migration check reads runtime-shaped YAML input without the type system folding it back into the narrowed union.
…hat-AI#12737) * 🔊 fix: Preserve Log Metadata on Console for Warn/Error Levels The default console formatter discarded every structured metadata key on the winston info object — only `CONSOLE_JSON=true` preserved it. That meant failures emitted by the agents SDK (e.g. "Summarization LLM call failed") reached stdout without the provider, model, or underlying error attached, leaving users unable to diagnose the root cause. - Add `formatConsoleMeta` helper to serialize non-reserved metadata as a compact JSON trailer, with per-value string truncation and safe handling of circular references. - Append the metadata trailer to warn/error console lines; info/debug behavior is unchanged. - Relax `debugTraverse`'s debug-only gate so warn/error messages routed through the debug formatter also surface their metadata. - Add a `formatConsoleMeta` stub to the shared logger mock so existing tests keep working. * 🔐 fix: Also Redact Sensitive Patterns on Warn Console Lines The warn-level console output now includes a metadata trailer that may contain provider-returned error strings with embedded tokens or keys (e.g. `Bearer ...`, `sk-...`). Apply `redactMessage` to warn lines in addition to error, matching the new surface area. * 🔐 fix: Redact Sensitive Tokens Embedded in JSON Metadata Two gaps in the existing console redaction that became user-visible once warn/error lines started emitting structured metadata: 1. The OpenAI-key regex (`/^(sk-)[^\s]+/`) was anchored to start-of-line, so keys embedded inside JSON payloads (e.g. `{"apiKey":"sk-..."}`) were never redacted. Every console line begins with a timestamp, so the anchor effectively made this pattern dead code. 2. `formatConsoleMeta` stringified metadata values verbatim; a sensitive string value was only redacted by the whole-line regex pass, which missed the anchored `sk-` case above. Fix: - Drop the `^` anchor; add `/g` so every occurrence is redacted, not just the first. - Also exclude `"` and `'` from the token body so JSON-embedded values terminate at the closing quote rather than chewing into the next field. - Simplify `redactMessage` to apply patterns directly (dropping the `getMatchingSensitivePatterns` filter) — the filter used `.test()` which has stateful behavior on `/g` regexes and is no longer needed. - `formatConsoleMeta` now runs `redactMessage` over every string value before JSON serialization, so the metadata trailer is safe even on the warn path. - Add regression tests covering both fixes. Reviewed-by: Codex (P1 finding on PR LibreChat-AI#12737, commit 68c31b6). * 🔐 fix: Redact Metadata in debugTraverse for Warn and Error Relaxing the debug-only gate in debugTraverse (in commit 59371be) routed warn/error records through the traversal path, which emits leaf string values verbatim (via truncateLongStrings only). Because DEBUG_LOGGING defaults to true, those records are also written to the rotating debug log file — which means payloads like `{ auth: 'Bearer ...' }` or `{ openaiKey: 'sk-...' }` were persisted unredacted once my earlier change took effect. Apply redactMessage to the final formatted string when the level is warn or error. Debug-level behavior is unchanged (matching prior art). Includes regression tests covering error/warn redaction and debug-level preservation. Reviewed-by: Codex (P1 finding on PR LibreChat-AI#12737, commit e288f7f). * 🔐 fix: Anchor Secret Regexes at Word Boundaries to Prevent Over-Redaction Removing the `^` anchor in commit e288f7f let the OpenAI-key regex match anywhere in the line — including inside ordinary words like `task-runner` or `mask-value`, where `sk-` appears mid-word. Non-secret text was being rewritten to `task-[REDACTED]`, hiding real log content from operators. - Anchor every sensitive-key pattern with `\b` so matches only fire at word boundaries. - Constrain the OpenAI-key body to the documented charset (`[a-zA-Z0-9_-]+`) instead of the broader "not whitespace or quote" character class. - Add `&` to the `key=` exclusion so a query-string value stops at the next parameter separator. - Regression tests covering both the over-redaction cases (`task-runner`, `monkey=10`) and the intended redactions still firing. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit c09d293). * 🔐 fix: Redact Before Colorize To Survive ANSI Word-Boundary Interference The console pipeline runs `redactFormat → colorize({ all: true }) → printf`. With `all: true`, winston wraps `info.message` in ANSI escapes whose trailing `m` is a word character. That means `\b(Bearer )…` placed at the start of a colorized segment can fall on a (word,word) boundary and miss — the earlier line-wise `redactMessage(line)` pass in printf suffers the same issue because it runs after colorize. Extend `redactFormat` to run for `warn` in addition to `error`, operating on the raw pre-colorize `info.message` + `Symbol.for('message')` strings. The later in-printf `redactMessage(line)` stays as a backstop, but the primary redaction now happens where the regex can actually see the text. Metadata redaction already operates on the raw info object via `formatConsoleMeta`, so it was never affected by ANSI — no change there. Includes regression tests for the new warn-level behavior and for the info/debug no-op path. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit fdb6b36). * 🧹 fix: Prefer Structured Metadata Over Consumed Splat Args in Traversal `debugTraverse` previously read `metadata[Symbol.for('splat')][0]` first and only fell back to the structured metadata object. When a caller uses printf interpolation alongside a metadata object — for example `logger.warn('failed for %s', tenant, { provider })` — winston leaves the *consumed* positional arg (`tenant`) in `SPLAT[0]` after interpolation. The formatter would then append the tenant a second time and skip the real metadata, regressing debug-file and `DEBUG_CONSOLE` output quality now that warn/error share this path. Prefer the structured metadata object (via `extractMetaObject`) and only fall back to `SPLAT[0]` when there's nothing else, so the surviving log line surfaces the actual key/value pairs regardless of call shape. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit 1e43d63). * 🧹 fix: Skip Consumed Splat Primitives in Warn/Error Debug Traversal When no structured metadata is attached, winston still leaves consumed `%s` / `%d` arguments in `Symbol.for('splat')`. Previous fix preferred the structured object but still fell back to whatever sat at `SPLAT[0]` — so `logger.warn('failed for %s', tenantId)` emitted `failed for tenant-7 tenant-7` in the traversal path (debug file and `DEBUG_CONSOLE`), now regressed outside of `debug` level because warn/error share the path. Only accept the splat fallback when the value is a plain object or an array (structural data worth surfacing). Primitives there are almost certainly consumed printf args and get skipped. Regression tests cover the single-%s case and the array-as-metadata case (which still surfaces through splat). Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit bccbf11). * 🧹 fix: Skip Numeric Splat Keys When Extracting Log Metadata When a caller passes a primitive as the second argument — e.g. `logger.warn('Unhandled step creation type:', step.type)` — winston / `format.splat()` can leave character-index keys (`"0"`, `"1"`, …) on the `info` object. With the warn/error metadata trailer in play, those synthetic artifacts were being surfaced as bogus metadata, producing noisy console and debug-file output. Filter out numeric-string keys in `extractMetaObject` so only real metadata fields reach the trailer. Added a regression test. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit b34628d). * 🧹 fix: Preserve Unconsumed Primitive Splat Args in Debug Traversal The previous round dropped every primitive SPLAT[0] value to avoid duplicating consumed %s args, but that removed useful context from calls like \`logger.debug('prefix:', detail)\` where the primitive was never interpolated — users lost the \`detail\` value. Refine the heuristic: skip a primitive splat value only when it already appears inside the (post-interpolation) \`info.message\`; otherwise surface it. Arrays and objects continue to surface unconditionally. Regression test covers the 'prefix:', detail case. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit 6bf9548). * 🧹 fix: Traverse Filtered Metadata, Not Raw Metadata, In debugTraverse `debugTraverse` computes `extracted = extractMetaObject(metadata)` to strip reserved keys, underscore-prefixed internals, and numeric splat artifacts — but the later \`klona(metadata)\` + \`traverse\` path still read the raw object, putting all the filtered junk back into the rendered multi-line output. Clone and traverse \`debugValue\` (the already-filtered object) instead. Regression test exercises the case where numeric splat artifacts sit alongside a real metadata field. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit c29c18e). * 🧹 refactor: Split Warn/Error From Debug-Level Traversal in Parsers Retrofitting `debugTraverse`'s multi-line object walker to cover warn/error created a minefield of splat-interaction edge cases (numeric artifacts, consumed %s args, bogus `_`-prefix filtering, over-eager suppression of unconsumed primitives). Each fix kept introducing new corner cases. Split the two concerns instead: - Warn/error now emit a compact single-line JSON metadata trailer via `formatConsoleMeta`, then pass the full line through `redactMessage`. This mirrors what the console formatter already does, so behavior between the console and debug-file outputs stays consistent for warn/error — and none of the splat/traversal edge cases apply. - Debug level keeps its original code path verbatim (including the raw `metadata` traversal and SPLAT\[0\] fallback). No regressions from my earlier iterations. - `extractMetaObject` no longer filters underscore-prefixed keys, so legitimate fields like MongoDB `_id` still appear. Reserved winston keys and numeric splat artifacts remain filtered. Updated tests reflect the simpler contract (underscore preservation, single-line trailer expectations already covered). Reviewed-by: Codex (two P2 findings on PR LibreChat-AI#12737, commit 9ea1152: `_id` regression and over-eager primitive suppression). * 🧹 fix: Preserve Scalar Metadata When One Value Is Circular `formatConsoleMeta` previously wrapped a single `JSON.stringify` in try/catch — any circular reference inside any field (e.g. an attached request/response object) caused the entire trailer to be dropped. That defeats the goal of making failures diagnosable: one malformed field would mask the provider/model/status we wanted to surface. Use a `WeakSet`-based replacer that emits `[Circular]` for repeated object visits. On the whole-object serialization failing, fall back to per-field serialization so scalar keys always land and only the offending field is replaced with \`"[Unserializable]"\`. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit d63742a). * 🧹 fix: Address Audit Findings (JSDoc, Case-Insensitive Api-Key, Tests) Audit review identified several MINOR/NIT items on top of the codex rounds. This commit closes the actionable ones: - **JSDoc (#1, #2)**: `extractMetaObject` no longer claims to filter underscore-prefixed keys (that filter was removed intentionally for MongoDB `_id`). `debugTraverse`'s docblock now describes the three code paths (warn/error compact trailer, debug multi-line traversal, other levels). - **Case-insensitive api-key regex (#6)**: `/gi` so the Azure style `Api-Key:` / `API-KEY:` also gets redacted. Pre-existing behavior was lowercase-only. - **Consolidated redundant branch (#5)**: `consoleFormat` printf was checking `isError || isWarn` twice; merged into one block. - **Pre-compiled regex (#9)**: `NUMERIC_KEY_RE` moved to module scope. - **Test coverage (#3, #4)**: Added regression tests for - per-field serialization fallback when a value's `toJSON` throws, - sensitive strings nested inside metadata objects, - the Azure-style `Api-Key:` header.
* 🤝 fix: load handoff sub-agents on OpenAI-compat endpoints (LibreChat-AI#12726) Extracts the BFS discovery + ACL-gated initialization of handoff sub-agents into a shared `discoverConnectedAgents` helper in `@librechat/api` and wires it into the OpenAI-compatible `/v1/chat/completions` and Open Responses `/v1/responses` controllers. These endpoints previously only passed the primary agent config to `createRun` while keeping `primaryConfig.edges` intact, which forced `MultiAgentGraph` into multi-agent mode without loading the referenced sub-agents and caused StateGraph to throw "Found edge ending at unknown node <id>". The discovery helper also filters orphaned edges (deleted sub-agents or those the caller lacks VIEW permission on), so API users see the same graceful fallback the chat UI already had. * 🧪 fix: use ServerRequest in discovery spec helpers CI `tsc --noEmit -p packages/api/tsconfig.json` caught that the test helpers typed `req` as `express.Request`, which is not assignable to `DiscoverConnectedAgentsParams.req` (typed as `ServerRequest` whose `user` is `IUser`). Local jest passed because ts-jest is transpile-only, but the CI typecheck uses the full compiler. * 🪲 fix: drop orphan edges on both endpoints, not just `to` Addresses the P1 codex finding on LibreChat-AI#12740: `filterOrphanedEdges` previously only removed edges whose `to` referenced a skipped agent. Edges whose `from` was a skipped agent — the symmetric case in a bidirectional graph like `A <-> B` where `B` is deleted or the user lacks VIEW on it — leaked through to `createRun` and re-triggered `Found edge ending at unknown node <id>` at StateGraph compile time. The filter now drops an edge if either endpoint references a skipped id, and the existing `to`-only test cases were updated to reflect the stricter behavior. Adds a bidirectional-graph regression test in `discovery.spec.ts`. * 🔒 fix: enforce REMOTE_AGENT ACL on handoff sub-agents for API routes Addresses the second P1 codex finding on LibreChat-AI#12740: the OpenAI-compat `/v1/chat/completions` and Open Responses `/v1/responses` routes gate the primary agent on `REMOTE_AGENT` (via `createCheckRemoteAgentAccess`), but `discoverConnectedAgents` was checking handoff sub-agents against the looser in-app `AGENT` resource type. That allowed a remote caller who could reach the orchestrator but had only in-app visibility on a sub-agent to invoke it via the API — bypassing the remote-sharing boundary. Adds an optional `resourceType` param to `discoverConnectedAgents` (defaulting to `AGENT` for the chat UI path) and passes `ResourceType.REMOTE_AGENT` from both API controllers so every discovered sub-agent clears the same sharing boundary enforced at route entry. * 🧯 fix: enforce allowedProviders for discovered sub-agents Addresses the third P1 codex finding on LibreChat-AI#12740: `discoverConnectedAgents` forwarded the caller's `endpointOption` verbatim into `initializeAgent`, but on the OpenAI-compat routes that option's `endpoint` is the primary agent's provider (e.g. `openai`), not `agents`. `initializeAgent` only enforces `allowedProviders` when `isAgentsEndpoint(endpointOption.endpoint)` is true, so handoff sub-agents silently bypassed the provider allowlist configured under `endpoints.agents.allowedProviders`. Override `endpointOption.endpoint` to `EModelEndpoint.agents` for every per-sub-agent init call. The primary agent still uses the caller's endpointOption as before — this only affects the BFS-loaded handoff targets. Regression test asserts the override. * ✂️ fix: prune unreachable sub-agents after orphan-edge filtering Addresses the fourth P1 codex finding on LibreChat-AI#12740: BFS eagerly initializes every sub-agent referenced in the primary's edge scan, but once `filterOrphanedEdges` drops edges whose endpoints were skipped, some of those sub-agents end up disconnected from the primary. In an `A -> B -> C` graph (edges stored directly on A) where B is skipped (missing or no VIEW), both edges are filtered, but C was already loaded and would still be passed to `createRun` — which flips into multi-agent mode on `agents.length > 1` and turns C into an unintended parallel start node. After filtering edges, compute the set of agent ids reachable from the primary through the surviving edge set and prune `agentConfigs` to that set. Two regression tests added: one for the pruning case, one that confirms agents connected via surviving edges are still kept. * 🔁 fix: don't seed initialize.js agentConfigs from the pre-pruning callback Addresses the fifth P1 codex finding on LibreChat-AI#12740: `onAgentInitialized` fires during BFS, BEFORE the helper prunes agents that become disconnected once `filterOrphanedEdges` runs. Writing the sub-agent straight into the outer `agentConfigs` there and then only additively merging the pruned `discoveredConfigs` left stranded entries in the outer map, and `AgentClient` would still hand them to `createRun` as extra parallel start nodes (the exact failure mode the pass-4 prune was meant to eliminate for the API controllers). Drop the `agentConfigs.set` from the callback and replace the additive merge with a direct copy from `discoveredConfigs`, which is now the single authoritative source of what the run should see. The per-agent tool context map is still populated during BFS — stale entries there are harmless because they're only read by closure inside `ON_TOOL_EXECUTE` and are unreachable once the agent is not in `agentConfigs`. * 🔬 fix: address audit findings on discovery helper Resolves findings from a comprehensive external audit of LibreChat-AI#12740. **Finding 1 (CRITICAL) — stale edges survive the reachability prune.** The pass-4 prune removed unreachable agents from `agentConfigs` but left matching edges in the return value. In an `A -> B -> C -> D` graph (all edges stored on A) where B is skipped, `filterOrphanedEdges` drops A->B and B->C but keeps C->D (neither endpoint is skipped). The caller then sees `agentConfigs` without C/D but `edges` still references them, flipping `createRun` into multi-agent mode with mismatched agents/edges — the exact crash this PR is supposed to fix. Now filter the edge list to the reachable set in the same pass, so the returned shape is self-consistent: every edge endpoint is either the primary id or a key of `agentConfigs`. New regression test covers A->B->C->D with B skipped. **Finding 2 (MAJOR) — unconditional `getModelsConfig` on every API request.** The OpenAI-compat and Responses controllers called `getModelsConfig(req)` and `discoverConnectedAgents` even when the primary agent had no edges (the common single-agent API case). Gate both behind `primaryConfig.edges?.length > 0` so single-agent runs don't pay that cost. **Finding 5 (MINOR) — silent mutation of caller's `primaryConfig.userMCPAuthMap`.** The helper aliased that object and then `Object.assign`'d sub-agent entries into it, changing the caller's config in-place. Shallow-clone up front so the returned merged map is the only destination. **Finding 7 (NIT) — dead `?? []` coalescing.** `filterOrphanedEdges` always returns a concrete array, so the `discoveredEdges ?? []` fallback was never reached. Simplified the `primaryConfig.edges = …` assignment. Also adds a test that verifies `primaryConfig.userMCPAuthMap` is not mutated in-place. * 🧹 chore: address audit NITs on discovery helper Addresses two NIT findings from the post-fix audit: **F1** — the shallow clone on `primaryConfig.userMCPAuthMap` was only applied on the primary side; the `else` branch (hit when the primary had no MCP auth and the first sub-agent seeds the map) assigned the sub-agent's `config.userMCPAuthMap` directly, so a later sub-agent's `Object.assign` mutated the first one's map in place. Harmless in practice (per-request ephemeral objects) but asymmetric. Clone in the else branch too. Test added. **F2** — `initialize.js` had a defensive `if (agentConfigs.size > 0 && !edges) edges = []` normalizer. Pre-existing dead code: the helper now always returns a concrete array from `filteredEdges.filter(...)`. Removed for clarity. * 🕸 fix: require all sources reachable when traversing fan-in edges Addresses the seventh P1 codex finding on LibreChat-AI#12740: the reachability BFS advanced through an edge as soon as any of its `from` endpoints matched the current frontier node (`sources.includes(current)`), but the subsequent edge filter required ALL sources to be reachable (`every`). The two-semantics mismatch let a fan-in edge like `{from: ['A','B'], to: 'C'}` mark C reachable purely via A even when B had no path from the primary, then drop the edge itself at filter time. Result: C survived in `agentConfigs` with no surviving edge connecting it to A, so `createRun` flipped into multi-agent mode on `agents.length > 1` and C ran as an unintended parallel root. Replace the BFS with a fixed-point iteration keyed on the same all-sources-reachable predicate used by the filter, so traversal and filtering stay aligned and multi-source edges only fire once every source is in the reachable set. Two regression tests added: - `{from: ['A','B'], to: 'C'}` with B having no incoming path — asserts neither B nor C leak into the result. - `A -> B`, `A -> C`, `['B','C'] -> D` — asserts the fan-in edge fires and D becomes reachable once both B and C are. * 🔀 fix: match SDK OR semantics for multi-source edge reachability Reverts the all-sources-required reachability gate from 4982f1c and replaces it with an any-source-reachable model, which matches how `@librechat/agents`'s `MultiAgentGraph.createWorkflow` actually wires multi-source edges at runtime (per-source `builder.addEdge(source, destination)`). With the previous `every` gate, a legitimate handoff edge `{ from: ['A', 'B'], to: 'C' }` where B had no incoming path was pruned along with C, regressing OR-semantics routing that the SDK would otherwise handle correctly. New behavior: 1. Reachability: an edge advances when ANY of its `from` endpoints is already reachable. Fixed-point iteration over `filteredEdges`. 2. Edge filter: keep an edge when it has at least one reachable source AND all destinations are reachable (a missing destination would still crash `StateGraph.compile` with `Found edge ending at unknown node`). 3. Agent prune: keep agents that are reachable OR referenced on any endpoint of a surviving edge. The second clause preserves co-sources in multi-source edges (B in `{ from: ['A','B'], to: 'C' }` when nothing else reaches B) so the SDK's per-source `addEdge` — and the `validateEdgeAgents` safety-net I added to the SDK in LibreChat-AI#111 — still finds B as a node. The pass-audit A->B->C->D regression test continues to pass: with B skipped, `filterOrphanedEdges` drops both B-adjacent edges, reachability never expands past A, C->D has no reachable source so it gets filtered, and C/D are pruned because they're neither reachable nor referenced. * ✂️ fix: strip skipped co-members from multi-source/multi-dest edges Addresses codex pass-9 P2 on LibreChat-AI#12740. `filterOrphanedEdges` previously dropped an edge whenever any `from` id was skipped, which was correct for scalar edges but over-aggressive for multi-source ones: the agents SDK adds one `builder.addEdge(source, destination)` per source, so `{ from: ['A','B'], to: 'C' }` with B skipped still has a valid `A -> C` route that was being thrown away. Now sanitize each endpoint: - Scalar skipped → drop the whole edge (no route survives). - Array with some skipped → strip the skipped ids, keep the edge with the surviving members. If the array empties out, drop the edge. Symmetric handling for `to` covers multi-destination fan-out when one co-destination is skipped. Tests updated/added: - `strips skipped co-sources from multi-source edges…` - `strips skipped co-destinations from multi-destination edges` - `drops multi-member edges only when every member on a side is skipped` - Discovery-side: `preserves valid routes when one co-source of a multi-source edge is skipped` asserts the end-to-end behavior — skipped co-source B gets stripped from the edge, A->C routing survives, and C remains in `agentConfigs`. * 🔓 fix: respect SHARE-on-AGENT fallback for handoff ACL on API routes Addresses codex pass-10 P1 on LibreChat-AI#12740. The API controllers were handing `discoverConnectedAgents` a raw `PermissionService.checkPermission` call against `ResourceType.REMOTE_AGENT`, but the route-level middleware (`createCheckRemoteAgentAccess`) authorizes the primary agent via `getRemoteAgentPermissions`, which first consults the AGENT ACL and treats owners with the SHARE bit as remotely authorized even without an explicit REMOTE_AGENT grant. The mismatch meant a user could open the primary via `/v1/chat/completions` or `/v1/responses`, but their own owned handoff sub-agents were silently skipped — breaking multi-agent handoffs for the common "owner runs their own multi-agent orchestrator" case. Both controllers now pass `discoverConnectedAgents` a `checkPermission` wrapper that delegates to `getRemoteAgentPermissions` (with `getEffectivePermissions` injected from `PermissionService`) and compares the returned bitmask against the required permission via `hasPermissions`. Sub-agents are now authorized by the exact same rules the route middleware applies to the primary. * 🌱 fix: preserve user-defined parallel-start branches Addresses codex pass-11 P2 on LibreChat-AI#12740. The post-filter reachability prune seeded only from `primaryConfig.id`, which killed `MultiAgentGraph`'s legitimate multi-start pattern — a user-defined edge like `X -> Y` where X has no incoming path (X is an intentional parallel starting node, run alongside the primary) was being dropped because neither X nor Y was reachable from the primary. Reconcile the tension with pass-4 ("prune accidental orphans when an intermediate is skipped") by using pre-filter reachability as the signal: - An agent that WAS reachable from the primary via the original (pre-filter) edges but loses that path when `filterOrphanedEdges` runs is an accidental orphan (a skipped hop broke the chain) — prune. - An agent that was NEVER reachable from the primary, even pre-filter, is an intentional parallel start — seed it into post-filter reachability so its component survives. Surviving-edge endpoint references still keep an agent (co-sources in multi-source edges). New test `preserves user-defined parallel-start branches disconnected from the primary` covers the pass-11 scenario; the existing `A->B->C->D, B skipped` regression test continues to pass because C/D were pre-filter reachable through B and lose that reachability after filtering. * 🎯 fix: tighten parallel-start seed criterion to 'no pre-filter incoming edge' Addresses codex pass-12 P1 on LibreChat-AI#12740. The pass-11 seed heuristic — 'agent is in `agentConfigs` but was not pre-filter reachable from the primary' — was too permissive. A downstream agent like Y in `X -> Y` where X gets skipped (missing / no VIEW) was never pre-filter reachable from the primary either, so the old rule promoted Y to a parallel start node and discovery returned `agents: [primary, Y]` with no connecting edge. The SDK then ran Y as an unintended parallel root — exactly the orphan behavior pass-4 wanted to prevent. Tighter criterion: seed a post-filter reachability root only when the agent had NO incoming edge in the pre-filter graph. That matches `MultiAgentGraph.analyzeGraph`'s "no-incoming-edge" definition of a start node applied to the user's original declared topology, so: - `A -> B` plus a user-defined `X -> Y` parallel branch: X has no incoming pre-filter → seeded → X and Y both survive. - `A -> B` plus `X -> Y` with X skipped: Y had an incoming pre-filter (`X -> Y`) → NOT seeded → Y is pruned as the orphan it is. - `A -> B -> C` with B skipped: C had an incoming pre-filter (`B -> C`) → NOT seeded → C is pruned. New test `does not promote a downstream orphan to a parallel start when its only upstream is skipped` locks in the pass-12 scenario. The pass-11 `preserves user-defined parallel-start branches` test continues to hold. * 📁 fix: don't enforce AGENT-only file ACL on REMOTE_AGENT API callers Addresses codex pass-13 P1 on LibreChat-AI#12740. When I refactored the API controllers' DB-method bundle, I inadvertently started forwarding `filterFilesByAgentAccess` into `initializeAgent`. That helper calls `checkPermission` with `resourceType: ResourceType.AGENT`, but these routes authorize callers through `REMOTE_AGENT` (via `getRemoteAgentPermissions`). A user granted `REMOTE_AGENT_VIEWER` on a shared agent but lacking direct `AGENT_VIEW` could invoke the agent yet all its owner-attached context files would get silently filtered out — breaking `file_search`/context retrieval for remote consumers. Drop `filterFilesByAgentAccess` from the OpenAI-compat and Responses controllers' `dbMethods` (and remove the now-unused import). The chat UI's `initialize.js` keeps it since that path legitimately authorizes at the AGENT level. No functional change inside the helper — passing `undefined` simply tells `primeResources` to skip the per-file ACL filter, restoring the pre-refactor API behavior. * 🪓 fix: strip unreachable co-sources from surviving multi-source edges Addresses codex pass-14 P1 on LibreChat-AI#12740. The earlier pass-8 fix kept any agent referenced as an endpoint of a surviving edge (via a `referencedByEdge` fallback) to avoid the SDK's `validateEdgeAgents` failing on missing nodes. But that fallback propped up unreachable co-sources too: with `[A -> C, X -> B, [B,C] -> D]` and X skipped, `X -> B` gets filtered, the `[B,C] -> D` fan-in survives because C is reachable, and B stays in `agentConfigs` solely because the fan-in still lists it. `MultiAgentGraph.analyzeGraph` then sees B with no incoming edge and runs it as an unintended parallel root. Sanitize surviving edges instead: for a kept edge whose `from` is an array, filter out any co-source that isn't reachable. The SDK's per-source `addEdge` fires independently, so dropping an unreachable co-source doesn't invalidate the remaining routes — in the scenario above `[B,C] -> D` becomes `[C] -> D`, every endpoint of every surviving edge is now reachable, and the agent prune collapses to a strict `reachable.has(agentId)` check. No more referenced-by-edge fallback. Regression test added: `strips unreachable co-sources from surviving multi-source edges (no stray parallel root)` — asserts B is absent from every surviving edge endpoint and the fan-in's `from` is just `['C']`. All 22 prior discovery tests still pass unchanged.
* 🦉 feat: Claude Opus 4.7 Model Support
- Add `claude-opus-4-7` to shared Anthropic models and `anthropic.claude-opus-4-7` to Bedrock models
- Register 1M context window and 128K max output in anthropic token maps
- Add token pricing ($5/$25), cache rates (6.25/0.5), and premium tier ($10/$37.50 above 200K) in tx.ts
- Update `.env.example` with Opus 4.7 IDs in `ANTHROPIC_MODELS` and `BEDROCK_AWS_MODELS` examples
- Add parallel Opus 4.7 test cases for token/cache/premium rates, context length, max output, name-variation matching, and 1M-context qualification
* feat: Add `xhigh` Effort Level for Opus 4.7
- Add `xhigh` variant to `AnthropicEffort` enum between `high` and `max`
- Expose `xhigh` in `anthropicSettings.effort.options` and the UI slider `enumMappings`
- Reuse existing `com_ui_xhigh` translation key
* test: Cover `xhigh` Effort and Exact Opus 4.7 Premium Rates
- Assert `xhigh` position (between high and max), inclusion in
`anthropicSettings.effort.options`, zod acceptance, and rejection of
unknown values in schemas.spec.ts
- Verify bedrockInputParser emits `output_config: { effort: 'xhigh' }`
for adaptive `anthropic.claude-opus-4-7`
- Verify getLLMConfig sets adaptive thinking and `output_config.effort =
'xhigh'` for `claude-opus-4-7`
- Pin Opus 4.7 premium pricing to exact threshold/prompt/completion
values (200000 / 10 / 37.5) so silent rate drift fails the test
* 🫧 fix: Restore Claude Opus 4.7 Reasoning Visibility
Claude Opus 4.7 omits `thinking` content from Messages API responses by
default — empty thinking blocks still stream, but the `thinking` field is
blank unless the caller passes `display: "summarized"` in the adaptive
thinking config. This silenced the LibreChat "Thoughts" UI for Anthropic
(and Anthropic-on-Bedrock) adaptive models.
- Extend `ThinkingConfigAdaptive` in `packages/api/src/types/anthropic.ts`
with an optional `display: 'summarized' | 'omitted'` field
- Emit `{ type: 'adaptive', display: 'summarized' }` from
`configureReasoning` in `packages/api/src/endpoints/anthropic/helpers.ts`
- Emit `{ type: 'adaptive', display: 'summarized' }` from
`bedrockInputParser` in `packages/data-provider/src/bedrock.ts` and
update the local `ThinkingConfig` union
- Update existing adaptive-thinking assertions to include the new field
- Add dedicated tests asserting `display: 'summarized'` flows through
both the Anthropic endpoint and the Bedrock parser
See https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7#thinking-content-omitted-by-default
* refactor: Gate `display: summarized` on Opus 4.7+
Narrow the reasoning-visibility opt-in to the models that actually omit
thinking content by default, instead of applying it to every adaptive
model. Pre-Opus-4.7 adaptive models (Opus 4.6, Sonnet 4.6) already return
summaries, so sending the field is unnecessary noise.
- Add `omitsThinkingByDefault(model)` in `packages/data-provider/src/bedrock.ts`
that returns true only for Opus 4.7+ (including future majors like Opus 5+)
- Bedrock parser now only attaches `display: 'summarized'` when the helper
matches, keeping the adaptive object unchanged for older models
- Anthropic endpoint `configureReasoning` uses the same helper so its emit
path matches the Bedrock one
- Tests: replace the blanket `display: 'summarized'` assertions with
model-specific ones (Opus 4.7 gets it, Opus 4.6 / Sonnet 4.6 do not),
add a dedicated `omitsThinkingByDefault` suite covering naming variants
and future versions
* feat: Configurable Thought Visibility for Anthropic Adaptive Models
Expose the Anthropic `thinking.display` API field as a user-facing
parameter so users can override the `auto` default (which stays as the
Opus-4.7+ opt-in added earlier in this PR). Also fixes the CI type error
by widening the adaptive thinking type assignment via a resolver helper
that returns a properly-typed object.
- Add `ThinkingDisplay` enum (`auto` | `summarized` | `omitted`) and
matching zod schema in `packages/data-provider/src/schemas.ts`
- Add `thinkingDisplay` to `tConversationSchema`, `anthropicSettings`, and
the pick lists for Bedrock input/parser + Anthropic agent params
- Add `resolveThinkingDisplay(model, explicit)` helper in
`packages/data-provider/src/bedrock.ts` that returns the wire value or
undefined (auto → model default, explicit → always honored)
- `bedrockInputParser` now reads `thinkingDisplay` from input and emits
`display` only when the resolver returns a value; strips the field
on non-adaptive-model branches so it does not leak
- `configureReasoning` in the Anthropic endpoint threads
`thinkingDisplay` through, uses the resolver, and casts the adaptive
config to `AnthropicClientOptions['thinking']` so the widened shape
compiles against the stale installed SDK types
- Add UI slider for `thinkingDisplay` in `parameterSettings.ts` next to
`effort`, with three-position `com_ui_auto` / `com_ui_summarized` /
`com_ui_omitted` labels
- Add translation keys `com_endpoint_anthropic_thinking_display`,
`com_endpoint_anthropic_thinking_display_desc`, `com_ui_summarized`,
`com_ui_omitted`
- Add tests: `resolveThinkingDisplay` suite (5 cases covering auto /
explicit / unknown input), parser round-trip tests for all three
modes on Opus 4.6 and Opus 4.7, Anthropic endpoint tests for explicit
summarized/omitted overrides
* fix: Drop `thinkingDisplay` When Adaptive Thinking Is Disabled
If a user turns adaptive thinking off but had previously selected a
`thinkingDisplay` value, the stale field was left in `additionalFields`
and ended up merged into the Bedrock request's
`additionalModelRequestFields`. That leaks a non-Bedrock key into the
payload and can round-trip back into `llmConfig`.
- Delete `additionalFields.thinkingDisplay` alongside `thinking` and
`thinkingBudget` in the `thinking === false` branch of
`bedrockInputParser`
- Add a regression test asserting `thinking`, `thinkingBudget`, and
`thinkingDisplay` are all absent when adaptive thinking is disabled on
an Opus 4.7 request
Reported by chatgpt-codex-connector on PR LibreChat-AI#12701.
* refactor: Consolidate `ThinkingDisplay` Types and Preserve Persisted Display
Address review findings on PR LibreChat-AI#12701:
- [Codex P2] `bedrockInputSchema.transform` now extracts
`thinking.display` from persisted `additionalModelRequestFields` back
into the top-level `thinkingDisplay` field so explicit `'omitted'`
round-trips through storage instead of being silently reverted to
`'summarized'` on the next parse.
- [Codex P2] `getLLMConfig` in the Anthropic endpoint now reads
`.display` from a persisted `thinking` object (agents store the full
Anthropic shape) and uses it as the fallback for `thinkingDisplay`
when no top-level override is present.
- [Audit #2] Collapse the three parallel wire-value types into a single
`ThinkingDisplayWireValue = Exclude<ThinkingDisplay, 'auto'>` exported
from `schemas.ts`; remove the duplicate `ThinkingDisplay` alias in
`packages/api/src/types/anthropic.ts` (which collided with the enum
name) and the `ThinkingDisplayValue` alias in `bedrock.ts`.
- [Audit #3] Add `thinkingDisplay` to the `TEndpointOption` pick list
next to `effort`.
- [Audit #4] Add a TODO comment next to the `as
AnthropicClientOptions['thinking']` cast explaining the stale
`@librechat/agents` SDK types that require it.
- Add tests: four round-trip cases asserting `bedrockInputSchema`
recovers `display` from persisted AMRF (Opus 4.7 omitted, pre-4.7
summarized, unknown-value ignore, explicit top-level wins), and two
`getLLMConfig` cases asserting the Anthropic endpoint preserves and
overrides persisted `thinking.display`.
* fix: Preserve Persisted `thinking.display` in bedrockInputParser
The parser constructed a fresh adaptive thinking config without looking at
any `display` already embedded in the incoming
`additionalModelRequestFields.thinking`. On round-trip through
`initializeBedrock`, a persisted user choice of `'omitted'` on Opus 4.7+
was silently reverted to `'summarized'` by the auto fallback.
- Extract `extractPersistedDisplay` helper and reuse it in both the
schema transform (form-state round-trip) and the parser (wire-request
round-trip)
- `bedrockInputParser` now feeds the persisted display as the resolver's
explicit value when no top-level `thinkingDisplay` override is set
- Add regression tests: parser preserves `display: 'omitted'` for
persisted Opus 4.7 AMRF, and top-level `thinkingDisplay` still wins
over persisted AMRF display
Reported by chatgpt-codex-connector (P1) on PR LibreChat-AI#12701.
krgokul
pushed a commit
that referenced
this pull request
Apr 20, 2026
* 🫧 fix: Restore Claude Opus 4.7 Reasoning Visibility
Claude Opus 4.7 omits `thinking` content from Messages API responses by
default — empty thinking blocks still stream, but the `thinking` field is
blank unless the caller passes `display: "summarized"` in the adaptive
thinking config. This silenced the LibreChat "Thoughts" UI for Anthropic
(and Anthropic-on-Bedrock) adaptive models.
- Extend `ThinkingConfigAdaptive` in `packages/api/src/types/anthropic.ts`
with an optional `display: 'summarized' | 'omitted'` field
- Emit `{ type: 'adaptive', display: 'summarized' }` from
`configureReasoning` in `packages/api/src/endpoints/anthropic/helpers.ts`
- Emit `{ type: 'adaptive', display: 'summarized' }` from
`bedrockInputParser` in `packages/data-provider/src/bedrock.ts` and
update the local `ThinkingConfig` union
- Update existing adaptive-thinking assertions to include the new field
- Add dedicated tests asserting `display: 'summarized'` flows through
both the Anthropic endpoint and the Bedrock parser
See https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7#thinking-content-omitted-by-default
* refactor: Gate `display: summarized` on Opus 4.7+
Narrow the reasoning-visibility opt-in to the models that actually omit
thinking content by default, instead of applying it to every adaptive
model. Pre-Opus-4.7 adaptive models (Opus 4.6, Sonnet 4.6) already return
summaries, so sending the field is unnecessary noise.
- Add `omitsThinkingByDefault(model)` in `packages/data-provider/src/bedrock.ts`
that returns true only for Opus 4.7+ (including future majors like Opus 5+)
- Bedrock parser now only attaches `display: 'summarized'` when the helper
matches, keeping the adaptive object unchanged for older models
- Anthropic endpoint `configureReasoning` uses the same helper so its emit
path matches the Bedrock one
- Tests: replace the blanket `display: 'summarized'` assertions with
model-specific ones (Opus 4.7 gets it, Opus 4.6 / Sonnet 4.6 do not),
add a dedicated `omitsThinkingByDefault` suite covering naming variants
and future versions
* feat: Configurable Thought Visibility for Anthropic Adaptive Models
Expose the Anthropic `thinking.display` API field as a user-facing
parameter so users can override the `auto` default (which stays as the
Opus-4.7+ opt-in added earlier in this PR). Also fixes the CI type error
by widening the adaptive thinking type assignment via a resolver helper
that returns a properly-typed object.
- Add `ThinkingDisplay` enum (`auto` | `summarized` | `omitted`) and
matching zod schema in `packages/data-provider/src/schemas.ts`
- Add `thinkingDisplay` to `tConversationSchema`, `anthropicSettings`, and
the pick lists for Bedrock input/parser + Anthropic agent params
- Add `resolveThinkingDisplay(model, explicit)` helper in
`packages/data-provider/src/bedrock.ts` that returns the wire value or
undefined (auto → model default, explicit → always honored)
- `bedrockInputParser` now reads `thinkingDisplay` from input and emits
`display` only when the resolver returns a value; strips the field
on non-adaptive-model branches so it does not leak
- `configureReasoning` in the Anthropic endpoint threads
`thinkingDisplay` through, uses the resolver, and casts the adaptive
config to `AnthropicClientOptions['thinking']` so the widened shape
compiles against the stale installed SDK types
- Add UI slider for `thinkingDisplay` in `parameterSettings.ts` next to
`effort`, with three-position `com_ui_auto` / `com_ui_summarized` /
`com_ui_omitted` labels
- Add translation keys `com_endpoint_anthropic_thinking_display`,
`com_endpoint_anthropic_thinking_display_desc`, `com_ui_summarized`,
`com_ui_omitted`
- Add tests: `resolveThinkingDisplay` suite (5 cases covering auto /
explicit / unknown input), parser round-trip tests for all three
modes on Opus 4.6 and Opus 4.7, Anthropic endpoint tests for explicit
summarized/omitted overrides
* fix: Drop `thinkingDisplay` When Adaptive Thinking Is Disabled
If a user turns adaptive thinking off but had previously selected a
`thinkingDisplay` value, the stale field was left in `additionalFields`
and ended up merged into the Bedrock request's
`additionalModelRequestFields`. That leaks a non-Bedrock key into the
payload and can round-trip back into `llmConfig`.
- Delete `additionalFields.thinkingDisplay` alongside `thinking` and
`thinkingBudget` in the `thinking === false` branch of
`bedrockInputParser`
- Add a regression test asserting `thinking`, `thinkingBudget`, and
`thinkingDisplay` are all absent when adaptive thinking is disabled on
an Opus 4.7 request
Reported by chatgpt-codex-connector on PR LibreChat-AI#12701.
* refactor: Consolidate `ThinkingDisplay` Types and Preserve Persisted Display
Address review findings on PR LibreChat-AI#12701:
- [Codex P2] `bedrockInputSchema.transform` now extracts
`thinking.display` from persisted `additionalModelRequestFields` back
into the top-level `thinkingDisplay` field so explicit `'omitted'`
round-trips through storage instead of being silently reverted to
`'summarized'` on the next parse.
- [Codex P2] `getLLMConfig` in the Anthropic endpoint now reads
`.display` from a persisted `thinking` object (agents store the full
Anthropic shape) and uses it as the fallback for `thinkingDisplay`
when no top-level override is present.
- [Audit #2] Collapse the three parallel wire-value types into a single
`ThinkingDisplayWireValue = Exclude<ThinkingDisplay, 'auto'>` exported
from `schemas.ts`; remove the duplicate `ThinkingDisplay` alias in
`packages/api/src/types/anthropic.ts` (which collided with the enum
name) and the `ThinkingDisplayValue` alias in `bedrock.ts`.
- [Audit #3] Add `thinkingDisplay` to the `TEndpointOption` pick list
next to `effort`.
- [Audit #4] Add a TODO comment next to the `as
AnthropicClientOptions['thinking']` cast explaining the stale
`@librechat/agents` SDK types that require it.
- Add tests: four round-trip cases asserting `bedrockInputSchema`
recovers `display` from persisted AMRF (Opus 4.7 omitted, pre-4.7
summarized, unknown-value ignore, explicit top-level wins), and two
`getLLMConfig` cases asserting the Anthropic endpoint preserves and
overrides persisted `thinking.display`.
* fix: Preserve Persisted `thinking.display` in bedrockInputParser
The parser constructed a fresh adaptive thinking config without looking at
any `display` already embedded in the incoming
`additionalModelRequestFields.thinking`. On round-trip through
`initializeBedrock`, a persisted user choice of `'omitted'` on Opus 4.7+
was silently reverted to `'summarized'` by the auto fallback.
- Extract `extractPersistedDisplay` helper and reuse it in both the
schema transform (form-state round-trip) and the parser (wire-request
round-trip)
- `bedrockInputParser` now feeds the persisted display as the resolver's
explicit value when no top-level `thinkingDisplay` override is set
- Add regression tests: parser preserves `display: 'omitted'` for
persisted Opus 4.7 AMRF, and top-level `thinkingDisplay` still wins
over persisted AMRF display
Reported by chatgpt-codex-connector (P1) on PR LibreChat-AI#12701.
krgokul
pushed a commit
that referenced
this pull request
Apr 20, 2026
…hat-AI#12737) * 🔊 fix: Preserve Log Metadata on Console for Warn/Error Levels The default console formatter discarded every structured metadata key on the winston info object — only `CONSOLE_JSON=true` preserved it. That meant failures emitted by the agents SDK (e.g. "Summarization LLM call failed") reached stdout without the provider, model, or underlying error attached, leaving users unable to diagnose the root cause. - Add `formatConsoleMeta` helper to serialize non-reserved metadata as a compact JSON trailer, with per-value string truncation and safe handling of circular references. - Append the metadata trailer to warn/error console lines; info/debug behavior is unchanged. - Relax `debugTraverse`'s debug-only gate so warn/error messages routed through the debug formatter also surface their metadata. - Add a `formatConsoleMeta` stub to the shared logger mock so existing tests keep working. * 🔐 fix: Also Redact Sensitive Patterns on Warn Console Lines The warn-level console output now includes a metadata trailer that may contain provider-returned error strings with embedded tokens or keys (e.g. `Bearer ...`, `sk-...`). Apply `redactMessage` to warn lines in addition to error, matching the new surface area. * 🔐 fix: Redact Sensitive Tokens Embedded in JSON Metadata Two gaps in the existing console redaction that became user-visible once warn/error lines started emitting structured metadata: 1. The OpenAI-key regex (`/^(sk-)[^\s]+/`) was anchored to start-of-line, so keys embedded inside JSON payloads (e.g. `{"apiKey":"sk-..."}`) were never redacted. Every console line begins with a timestamp, so the anchor effectively made this pattern dead code. 2. `formatConsoleMeta` stringified metadata values verbatim; a sensitive string value was only redacted by the whole-line regex pass, which missed the anchored `sk-` case above. Fix: - Drop the `^` anchor; add `/g` so every occurrence is redacted, not just the first. - Also exclude `"` and `'` from the token body so JSON-embedded values terminate at the closing quote rather than chewing into the next field. - Simplify `redactMessage` to apply patterns directly (dropping the `getMatchingSensitivePatterns` filter) — the filter used `.test()` which has stateful behavior on `/g` regexes and is no longer needed. - `formatConsoleMeta` now runs `redactMessage` over every string value before JSON serialization, so the metadata trailer is safe even on the warn path. - Add regression tests covering both fixes. Reviewed-by: Codex (P1 finding on PR LibreChat-AI#12737, commit 68c31b6). * 🔐 fix: Redact Metadata in debugTraverse for Warn and Error Relaxing the debug-only gate in debugTraverse (in commit 59371be) routed warn/error records through the traversal path, which emits leaf string values verbatim (via truncateLongStrings only). Because DEBUG_LOGGING defaults to true, those records are also written to the rotating debug log file — which means payloads like `{ auth: 'Bearer ...' }` or `{ openaiKey: 'sk-...' }` were persisted unredacted once my earlier change took effect. Apply redactMessage to the final formatted string when the level is warn or error. Debug-level behavior is unchanged (matching prior art). Includes regression tests covering error/warn redaction and debug-level preservation. Reviewed-by: Codex (P1 finding on PR LibreChat-AI#12737, commit e288f7f). * 🔐 fix: Anchor Secret Regexes at Word Boundaries to Prevent Over-Redaction Removing the `^` anchor in commit e288f7f let the OpenAI-key regex match anywhere in the line — including inside ordinary words like `task-runner` or `mask-value`, where `sk-` appears mid-word. Non-secret text was being rewritten to `task-[REDACTED]`, hiding real log content from operators. - Anchor every sensitive-key pattern with `\b` so matches only fire at word boundaries. - Constrain the OpenAI-key body to the documented charset (`[a-zA-Z0-9_-]+`) instead of the broader "not whitespace or quote" character class. - Add `&` to the `key=` exclusion so a query-string value stops at the next parameter separator. - Regression tests covering both the over-redaction cases (`task-runner`, `monkey=10`) and the intended redactions still firing. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit c09d293). * 🔐 fix: Redact Before Colorize To Survive ANSI Word-Boundary Interference The console pipeline runs `redactFormat → colorize({ all: true }) → printf`. With `all: true`, winston wraps `info.message` in ANSI escapes whose trailing `m` is a word character. That means `\b(Bearer )…` placed at the start of a colorized segment can fall on a (word,word) boundary and miss — the earlier line-wise `redactMessage(line)` pass in printf suffers the same issue because it runs after colorize. Extend `redactFormat` to run for `warn` in addition to `error`, operating on the raw pre-colorize `info.message` + `Symbol.for('message')` strings. The later in-printf `redactMessage(line)` stays as a backstop, but the primary redaction now happens where the regex can actually see the text. Metadata redaction already operates on the raw info object via `formatConsoleMeta`, so it was never affected by ANSI — no change there. Includes regression tests for the new warn-level behavior and for the info/debug no-op path. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit fdb6b36). * 🧹 fix: Prefer Structured Metadata Over Consumed Splat Args in Traversal `debugTraverse` previously read `metadata[Symbol.for('splat')][0]` first and only fell back to the structured metadata object. When a caller uses printf interpolation alongside a metadata object — for example `logger.warn('failed for %s', tenant, { provider })` — winston leaves the *consumed* positional arg (`tenant`) in `SPLAT[0]` after interpolation. The formatter would then append the tenant a second time and skip the real metadata, regressing debug-file and `DEBUG_CONSOLE` output quality now that warn/error share this path. Prefer the structured metadata object (via `extractMetaObject`) and only fall back to `SPLAT[0]` when there's nothing else, so the surviving log line surfaces the actual key/value pairs regardless of call shape. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit 1e43d63). * 🧹 fix: Skip Consumed Splat Primitives in Warn/Error Debug Traversal When no structured metadata is attached, winston still leaves consumed `%s` / `%d` arguments in `Symbol.for('splat')`. Previous fix preferred the structured object but still fell back to whatever sat at `SPLAT[0]` — so `logger.warn('failed for %s', tenantId)` emitted `failed for tenant-7 tenant-7` in the traversal path (debug file and `DEBUG_CONSOLE`), now regressed outside of `debug` level because warn/error share the path. Only accept the splat fallback when the value is a plain object or an array (structural data worth surfacing). Primitives there are almost certainly consumed printf args and get skipped. Regression tests cover the single-%s case and the array-as-metadata case (which still surfaces through splat). Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit bccbf11). * 🧹 fix: Skip Numeric Splat Keys When Extracting Log Metadata When a caller passes a primitive as the second argument — e.g. `logger.warn('Unhandled step creation type:', step.type)` — winston / `format.splat()` can leave character-index keys (`"0"`, `"1"`, …) on the `info` object. With the warn/error metadata trailer in play, those synthetic artifacts were being surfaced as bogus metadata, producing noisy console and debug-file output. Filter out numeric-string keys in `extractMetaObject` so only real metadata fields reach the trailer. Added a regression test. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit b34628d). * 🧹 fix: Preserve Unconsumed Primitive Splat Args in Debug Traversal The previous round dropped every primitive SPLAT[0] value to avoid duplicating consumed %s args, but that removed useful context from calls like \`logger.debug('prefix:', detail)\` where the primitive was never interpolated — users lost the \`detail\` value. Refine the heuristic: skip a primitive splat value only when it already appears inside the (post-interpolation) \`info.message\`; otherwise surface it. Arrays and objects continue to surface unconditionally. Regression test covers the 'prefix:', detail case. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit 6bf9548). * 🧹 fix: Traverse Filtered Metadata, Not Raw Metadata, In debugTraverse `debugTraverse` computes `extracted = extractMetaObject(metadata)` to strip reserved keys, underscore-prefixed internals, and numeric splat artifacts — but the later \`klona(metadata)\` + \`traverse\` path still read the raw object, putting all the filtered junk back into the rendered multi-line output. Clone and traverse \`debugValue\` (the already-filtered object) instead. Regression test exercises the case where numeric splat artifacts sit alongside a real metadata field. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit c29c18e). * 🧹 refactor: Split Warn/Error From Debug-Level Traversal in Parsers Retrofitting `debugTraverse`'s multi-line object walker to cover warn/error created a minefield of splat-interaction edge cases (numeric artifacts, consumed %s args, bogus `_`-prefix filtering, over-eager suppression of unconsumed primitives). Each fix kept introducing new corner cases. Split the two concerns instead: - Warn/error now emit a compact single-line JSON metadata trailer via `formatConsoleMeta`, then pass the full line through `redactMessage`. This mirrors what the console formatter already does, so behavior between the console and debug-file outputs stays consistent for warn/error — and none of the splat/traversal edge cases apply. - Debug level keeps its original code path verbatim (including the raw `metadata` traversal and SPLAT\[0\] fallback). No regressions from my earlier iterations. - `extractMetaObject` no longer filters underscore-prefixed keys, so legitimate fields like MongoDB `_id` still appear. Reserved winston keys and numeric splat artifacts remain filtered. Updated tests reflect the simpler contract (underscore preservation, single-line trailer expectations already covered). Reviewed-by: Codex (two P2 findings on PR LibreChat-AI#12737, commit 9ea1152: `_id` regression and over-eager primitive suppression). * 🧹 fix: Preserve Scalar Metadata When One Value Is Circular `formatConsoleMeta` previously wrapped a single `JSON.stringify` in try/catch — any circular reference inside any field (e.g. an attached request/response object) caused the entire trailer to be dropped. That defeats the goal of making failures diagnosable: one malformed field would mask the provider/model/status we wanted to surface. Use a `WeakSet`-based replacer that emits `[Circular]` for repeated object visits. On the whole-object serialization failing, fall back to per-field serialization so scalar keys always land and only the offending field is replaced with \`"[Unserializable]"\`. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit d63742a). * 🧹 fix: Address Audit Findings (JSDoc, Case-Insensitive Api-Key, Tests) Audit review identified several MINOR/NIT items on top of the codex rounds. This commit closes the actionable ones: - **JSDoc (#1, #2)**: `extractMetaObject` no longer claims to filter underscore-prefixed keys (that filter was removed intentionally for MongoDB `_id`). `debugTraverse`'s docblock now describes the three code paths (warn/error compact trailer, debug multi-line traversal, other levels). - **Case-insensitive api-key regex (#6)**: `/gi` so the Azure style `Api-Key:` / `API-KEY:` also gets redacted. Pre-existing behavior was lowercase-only. - **Consolidated redundant branch (#5)**: `consoleFormat` printf was checking `isError || isWarn` twice; merged into one block. - **Pre-compiled regex (#9)**: `NUMERIC_KEY_RE` moved to module scope. - **Test coverage (#3, #4)**: Added regression tests for - per-field serialization fallback when a value's `toJSON` throws, - sensitive strings nested inside metadata objects, - the Azure-style `Api-Key:` header.
krgokul
pushed a commit
that referenced
this pull request
Apr 21, 2026
* 🫧 fix: Restore Claude Opus 4.7 Reasoning Visibility
Claude Opus 4.7 omits `thinking` content from Messages API responses by
default — empty thinking blocks still stream, but the `thinking` field is
blank unless the caller passes `display: "summarized"` in the adaptive
thinking config. This silenced the LibreChat "Thoughts" UI for Anthropic
(and Anthropic-on-Bedrock) adaptive models.
- Extend `ThinkingConfigAdaptive` in `packages/api/src/types/anthropic.ts`
with an optional `display: 'summarized' | 'omitted'` field
- Emit `{ type: 'adaptive', display: 'summarized' }` from
`configureReasoning` in `packages/api/src/endpoints/anthropic/helpers.ts`
- Emit `{ type: 'adaptive', display: 'summarized' }` from
`bedrockInputParser` in `packages/data-provider/src/bedrock.ts` and
update the local `ThinkingConfig` union
- Update existing adaptive-thinking assertions to include the new field
- Add dedicated tests asserting `display: 'summarized'` flows through
both the Anthropic endpoint and the Bedrock parser
See https://platform.claude.com/docs/en/about-claude/models/whats-new-claude-4-7#thinking-content-omitted-by-default
* refactor: Gate `display: summarized` on Opus 4.7+
Narrow the reasoning-visibility opt-in to the models that actually omit
thinking content by default, instead of applying it to every adaptive
model. Pre-Opus-4.7 adaptive models (Opus 4.6, Sonnet 4.6) already return
summaries, so sending the field is unnecessary noise.
- Add `omitsThinkingByDefault(model)` in `packages/data-provider/src/bedrock.ts`
that returns true only for Opus 4.7+ (including future majors like Opus 5+)
- Bedrock parser now only attaches `display: 'summarized'` when the helper
matches, keeping the adaptive object unchanged for older models
- Anthropic endpoint `configureReasoning` uses the same helper so its emit
path matches the Bedrock one
- Tests: replace the blanket `display: 'summarized'` assertions with
model-specific ones (Opus 4.7 gets it, Opus 4.6 / Sonnet 4.6 do not),
add a dedicated `omitsThinkingByDefault` suite covering naming variants
and future versions
* feat: Configurable Thought Visibility for Anthropic Adaptive Models
Expose the Anthropic `thinking.display` API field as a user-facing
parameter so users can override the `auto` default (which stays as the
Opus-4.7+ opt-in added earlier in this PR). Also fixes the CI type error
by widening the adaptive thinking type assignment via a resolver helper
that returns a properly-typed object.
- Add `ThinkingDisplay` enum (`auto` | `summarized` | `omitted`) and
matching zod schema in `packages/data-provider/src/schemas.ts`
- Add `thinkingDisplay` to `tConversationSchema`, `anthropicSettings`, and
the pick lists for Bedrock input/parser + Anthropic agent params
- Add `resolveThinkingDisplay(model, explicit)` helper in
`packages/data-provider/src/bedrock.ts` that returns the wire value or
undefined (auto → model default, explicit → always honored)
- `bedrockInputParser` now reads `thinkingDisplay` from input and emits
`display` only when the resolver returns a value; strips the field
on non-adaptive-model branches so it does not leak
- `configureReasoning` in the Anthropic endpoint threads
`thinkingDisplay` through, uses the resolver, and casts the adaptive
config to `AnthropicClientOptions['thinking']` so the widened shape
compiles against the stale installed SDK types
- Add UI slider for `thinkingDisplay` in `parameterSettings.ts` next to
`effort`, with three-position `com_ui_auto` / `com_ui_summarized` /
`com_ui_omitted` labels
- Add translation keys `com_endpoint_anthropic_thinking_display`,
`com_endpoint_anthropic_thinking_display_desc`, `com_ui_summarized`,
`com_ui_omitted`
- Add tests: `resolveThinkingDisplay` suite (5 cases covering auto /
explicit / unknown input), parser round-trip tests for all three
modes on Opus 4.6 and Opus 4.7, Anthropic endpoint tests for explicit
summarized/omitted overrides
* fix: Drop `thinkingDisplay` When Adaptive Thinking Is Disabled
If a user turns adaptive thinking off but had previously selected a
`thinkingDisplay` value, the stale field was left in `additionalFields`
and ended up merged into the Bedrock request's
`additionalModelRequestFields`. That leaks a non-Bedrock key into the
payload and can round-trip back into `llmConfig`.
- Delete `additionalFields.thinkingDisplay` alongside `thinking` and
`thinkingBudget` in the `thinking === false` branch of
`bedrockInputParser`
- Add a regression test asserting `thinking`, `thinkingBudget`, and
`thinkingDisplay` are all absent when adaptive thinking is disabled on
an Opus 4.7 request
Reported by chatgpt-codex-connector on PR LibreChat-AI#12701.
* refactor: Consolidate `ThinkingDisplay` Types and Preserve Persisted Display
Address review findings on PR LibreChat-AI#12701:
- [Codex P2] `bedrockInputSchema.transform` now extracts
`thinking.display` from persisted `additionalModelRequestFields` back
into the top-level `thinkingDisplay` field so explicit `'omitted'`
round-trips through storage instead of being silently reverted to
`'summarized'` on the next parse.
- [Codex P2] `getLLMConfig` in the Anthropic endpoint now reads
`.display` from a persisted `thinking` object (agents store the full
Anthropic shape) and uses it as the fallback for `thinkingDisplay`
when no top-level override is present.
- [Audit #2] Collapse the three parallel wire-value types into a single
`ThinkingDisplayWireValue = Exclude<ThinkingDisplay, 'auto'>` exported
from `schemas.ts`; remove the duplicate `ThinkingDisplay` alias in
`packages/api/src/types/anthropic.ts` (which collided with the enum
name) and the `ThinkingDisplayValue` alias in `bedrock.ts`.
- [Audit #3] Add `thinkingDisplay` to the `TEndpointOption` pick list
next to `effort`.
- [Audit #4] Add a TODO comment next to the `as
AnthropicClientOptions['thinking']` cast explaining the stale
`@librechat/agents` SDK types that require it.
- Add tests: four round-trip cases asserting `bedrockInputSchema`
recovers `display` from persisted AMRF (Opus 4.7 omitted, pre-4.7
summarized, unknown-value ignore, explicit top-level wins), and two
`getLLMConfig` cases asserting the Anthropic endpoint preserves and
overrides persisted `thinking.display`.
* fix: Preserve Persisted `thinking.display` in bedrockInputParser
The parser constructed a fresh adaptive thinking config without looking at
any `display` already embedded in the incoming
`additionalModelRequestFields.thinking`. On round-trip through
`initializeBedrock`, a persisted user choice of `'omitted'` on Opus 4.7+
was silently reverted to `'summarized'` by the auto fallback.
- Extract `extractPersistedDisplay` helper and reuse it in both the
schema transform (form-state round-trip) and the parser (wire-request
round-trip)
- `bedrockInputParser` now feeds the persisted display as the resolver's
explicit value when no top-level `thinkingDisplay` override is set
- Add regression tests: parser preserves `display: 'omitted'` for
persisted Opus 4.7 AMRF, and top-level `thinkingDisplay` still wins
over persisted AMRF display
Reported by chatgpt-codex-connector (P1) on PR LibreChat-AI#12701.
krgokul
pushed a commit
that referenced
this pull request
Apr 21, 2026
…hat-AI#12737) * 🔊 fix: Preserve Log Metadata on Console for Warn/Error Levels The default console formatter discarded every structured metadata key on the winston info object — only `CONSOLE_JSON=true` preserved it. That meant failures emitted by the agents SDK (e.g. "Summarization LLM call failed") reached stdout without the provider, model, or underlying error attached, leaving users unable to diagnose the root cause. - Add `formatConsoleMeta` helper to serialize non-reserved metadata as a compact JSON trailer, with per-value string truncation and safe handling of circular references. - Append the metadata trailer to warn/error console lines; info/debug behavior is unchanged. - Relax `debugTraverse`'s debug-only gate so warn/error messages routed through the debug formatter also surface their metadata. - Add a `formatConsoleMeta` stub to the shared logger mock so existing tests keep working. * 🔐 fix: Also Redact Sensitive Patterns on Warn Console Lines The warn-level console output now includes a metadata trailer that may contain provider-returned error strings with embedded tokens or keys (e.g. `Bearer ...`, `sk-...`). Apply `redactMessage` to warn lines in addition to error, matching the new surface area. * 🔐 fix: Redact Sensitive Tokens Embedded in JSON Metadata Two gaps in the existing console redaction that became user-visible once warn/error lines started emitting structured metadata: 1. The OpenAI-key regex (`/^(sk-)[^\s]+/`) was anchored to start-of-line, so keys embedded inside JSON payloads (e.g. `{"apiKey":"sk-..."}`) were never redacted. Every console line begins with a timestamp, so the anchor effectively made this pattern dead code. 2. `formatConsoleMeta` stringified metadata values verbatim; a sensitive string value was only redacted by the whole-line regex pass, which missed the anchored `sk-` case above. Fix: - Drop the `^` anchor; add `/g` so every occurrence is redacted, not just the first. - Also exclude `"` and `'` from the token body so JSON-embedded values terminate at the closing quote rather than chewing into the next field. - Simplify `redactMessage` to apply patterns directly (dropping the `getMatchingSensitivePatterns` filter) — the filter used `.test()` which has stateful behavior on `/g` regexes and is no longer needed. - `formatConsoleMeta` now runs `redactMessage` over every string value before JSON serialization, so the metadata trailer is safe even on the warn path. - Add regression tests covering both fixes. Reviewed-by: Codex (P1 finding on PR LibreChat-AI#12737, commit 68c31b6). * 🔐 fix: Redact Metadata in debugTraverse for Warn and Error Relaxing the debug-only gate in debugTraverse (in commit 59371be) routed warn/error records through the traversal path, which emits leaf string values verbatim (via truncateLongStrings only). Because DEBUG_LOGGING defaults to true, those records are also written to the rotating debug log file — which means payloads like `{ auth: 'Bearer ...' }` or `{ openaiKey: 'sk-...' }` were persisted unredacted once my earlier change took effect. Apply redactMessage to the final formatted string when the level is warn or error. Debug-level behavior is unchanged (matching prior art). Includes regression tests covering error/warn redaction and debug-level preservation. Reviewed-by: Codex (P1 finding on PR LibreChat-AI#12737, commit e288f7f). * 🔐 fix: Anchor Secret Regexes at Word Boundaries to Prevent Over-Redaction Removing the `^` anchor in commit e288f7f let the OpenAI-key regex match anywhere in the line — including inside ordinary words like `task-runner` or `mask-value`, where `sk-` appears mid-word. Non-secret text was being rewritten to `task-[REDACTED]`, hiding real log content from operators. - Anchor every sensitive-key pattern with `\b` so matches only fire at word boundaries. - Constrain the OpenAI-key body to the documented charset (`[a-zA-Z0-9_-]+`) instead of the broader "not whitespace or quote" character class. - Add `&` to the `key=` exclusion so a query-string value stops at the next parameter separator. - Regression tests covering both the over-redaction cases (`task-runner`, `monkey=10`) and the intended redactions still firing. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit c09d293). * 🔐 fix: Redact Before Colorize To Survive ANSI Word-Boundary Interference The console pipeline runs `redactFormat → colorize({ all: true }) → printf`. With `all: true`, winston wraps `info.message` in ANSI escapes whose trailing `m` is a word character. That means `\b(Bearer )…` placed at the start of a colorized segment can fall on a (word,word) boundary and miss — the earlier line-wise `redactMessage(line)` pass in printf suffers the same issue because it runs after colorize. Extend `redactFormat` to run for `warn` in addition to `error`, operating on the raw pre-colorize `info.message` + `Symbol.for('message')` strings. The later in-printf `redactMessage(line)` stays as a backstop, but the primary redaction now happens where the regex can actually see the text. Metadata redaction already operates on the raw info object via `formatConsoleMeta`, so it was never affected by ANSI — no change there. Includes regression tests for the new warn-level behavior and for the info/debug no-op path. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit fdb6b36). * 🧹 fix: Prefer Structured Metadata Over Consumed Splat Args in Traversal `debugTraverse` previously read `metadata[Symbol.for('splat')][0]` first and only fell back to the structured metadata object. When a caller uses printf interpolation alongside a metadata object — for example `logger.warn('failed for %s', tenant, { provider })` — winston leaves the *consumed* positional arg (`tenant`) in `SPLAT[0]` after interpolation. The formatter would then append the tenant a second time and skip the real metadata, regressing debug-file and `DEBUG_CONSOLE` output quality now that warn/error share this path. Prefer the structured metadata object (via `extractMetaObject`) and only fall back to `SPLAT[0]` when there's nothing else, so the surviving log line surfaces the actual key/value pairs regardless of call shape. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit 1e43d63). * 🧹 fix: Skip Consumed Splat Primitives in Warn/Error Debug Traversal When no structured metadata is attached, winston still leaves consumed `%s` / `%d` arguments in `Symbol.for('splat')`. Previous fix preferred the structured object but still fell back to whatever sat at `SPLAT[0]` — so `logger.warn('failed for %s', tenantId)` emitted `failed for tenant-7 tenant-7` in the traversal path (debug file and `DEBUG_CONSOLE`), now regressed outside of `debug` level because warn/error share the path. Only accept the splat fallback when the value is a plain object or an array (structural data worth surfacing). Primitives there are almost certainly consumed printf args and get skipped. Regression tests cover the single-%s case and the array-as-metadata case (which still surfaces through splat). Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit bccbf11). * 🧹 fix: Skip Numeric Splat Keys When Extracting Log Metadata When a caller passes a primitive as the second argument — e.g. `logger.warn('Unhandled step creation type:', step.type)` — winston / `format.splat()` can leave character-index keys (`"0"`, `"1"`, …) on the `info` object. With the warn/error metadata trailer in play, those synthetic artifacts were being surfaced as bogus metadata, producing noisy console and debug-file output. Filter out numeric-string keys in `extractMetaObject` so only real metadata fields reach the trailer. Added a regression test. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit b34628d). * 🧹 fix: Preserve Unconsumed Primitive Splat Args in Debug Traversal The previous round dropped every primitive SPLAT[0] value to avoid duplicating consumed %s args, but that removed useful context from calls like \`logger.debug('prefix:', detail)\` where the primitive was never interpolated — users lost the \`detail\` value. Refine the heuristic: skip a primitive splat value only when it already appears inside the (post-interpolation) \`info.message\`; otherwise surface it. Arrays and objects continue to surface unconditionally. Regression test covers the 'prefix:', detail case. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit 6bf9548). * 🧹 fix: Traverse Filtered Metadata, Not Raw Metadata, In debugTraverse `debugTraverse` computes `extracted = extractMetaObject(metadata)` to strip reserved keys, underscore-prefixed internals, and numeric splat artifacts — but the later \`klona(metadata)\` + \`traverse\` path still read the raw object, putting all the filtered junk back into the rendered multi-line output. Clone and traverse \`debugValue\` (the already-filtered object) instead. Regression test exercises the case where numeric splat artifacts sit alongside a real metadata field. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit c29c18e). * 🧹 refactor: Split Warn/Error From Debug-Level Traversal in Parsers Retrofitting `debugTraverse`'s multi-line object walker to cover warn/error created a minefield of splat-interaction edge cases (numeric artifacts, consumed %s args, bogus `_`-prefix filtering, over-eager suppression of unconsumed primitives). Each fix kept introducing new corner cases. Split the two concerns instead: - Warn/error now emit a compact single-line JSON metadata trailer via `formatConsoleMeta`, then pass the full line through `redactMessage`. This mirrors what the console formatter already does, so behavior between the console and debug-file outputs stays consistent for warn/error — and none of the splat/traversal edge cases apply. - Debug level keeps its original code path verbatim (including the raw `metadata` traversal and SPLAT\[0\] fallback). No regressions from my earlier iterations. - `extractMetaObject` no longer filters underscore-prefixed keys, so legitimate fields like MongoDB `_id` still appear. Reserved winston keys and numeric splat artifacts remain filtered. Updated tests reflect the simpler contract (underscore preservation, single-line trailer expectations already covered). Reviewed-by: Codex (two P2 findings on PR LibreChat-AI#12737, commit 9ea1152: `_id` regression and over-eager primitive suppression). * 🧹 fix: Preserve Scalar Metadata When One Value Is Circular `formatConsoleMeta` previously wrapped a single `JSON.stringify` in try/catch — any circular reference inside any field (e.g. an attached request/response object) caused the entire trailer to be dropped. That defeats the goal of making failures diagnosable: one malformed field would mask the provider/model/status we wanted to surface. Use a `WeakSet`-based replacer that emits `[Circular]` for repeated object visits. On the whole-object serialization failing, fall back to per-field serialization so scalar keys always land and only the offending field is replaced with \`"[Unserializable]"\`. Reviewed-by: Codex (P2 finding on PR LibreChat-AI#12737, commit d63742a). * 🧹 fix: Address Audit Findings (JSDoc, Case-Insensitive Api-Key, Tests) Audit review identified several MINOR/NIT items on top of the codex rounds. This commit closes the actionable ones: - **JSDoc (#1, #2)**: `extractMetaObject` no longer claims to filter underscore-prefixed keys (that filter was removed intentionally for MongoDB `_id`). `debugTraverse`'s docblock now describes the three code paths (warn/error compact trailer, debug multi-line traversal, other levels). - **Case-insensitive api-key regex (#6)**: `/gi` so the Azure style `Api-Key:` / `API-KEY:` also gets redacted. Pre-existing behavior was lowercase-only. - **Consolidated redundant branch (#5)**: `consoleFormat` printf was checking `isError || isWarn` twice; merged into one block. - **Pre-compiled regex (#9)**: `NUMERIC_KEY_RE` moved to module scope. - **Test coverage (#3, #4)**: Added regression tests for - per-field serialization fallback when a value's `toJSON` throws, - sensitive strings nested inside metadata objects, - the Azure-style `Api-Key:` header.
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Pull Request Template
Summary
Please provide a brief summary of your changes and the related issue. Include any motivation and context that is relevant to your changes. If there are any dependencies necessary for your changes, please list them here.
Change Type
Please delete any irrelevant options.
Testing
Please describe your test process and include instructions so that we can reproduce your test. If there are any important variables for your testing configuration, list them here.
Test Configuration:
Checklist
Please delete any irrelevant options.