@librechat/agents 3.2.63 → 3.2.64

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -88,6 +88,29 @@ function isSerializableBedrockReasoningBlock(
88
88
  return content.redactedContent != null && content.redactedContent !== '';
89
89
  }
90
90
 
91
+ function appendSerializableBedrockTextBlock(
92
+ contentBlocks: BedrockContentBlock[],
93
+ text: string
94
+ ): boolean {
95
+ if (text === '') {
96
+ return false;
97
+ }
98
+ const cleanedText = text.replace(/\n/g, '').trim();
99
+ if (cleanedText !== '') {
100
+ contentBlocks.push({ text });
101
+ return true;
102
+ }
103
+ const lastBlock = contentBlocks[contentBlocks.length - 1] as
104
+ | BedrockContentBlock
105
+ | undefined;
106
+ if (lastBlock == null || !('text' in lastBlock)) {
107
+ return false;
108
+ }
109
+ const mergedTextContent = `${lastBlock.text}${text}`;
110
+ (lastBlock as { text: string }).text = mergedTextContent;
111
+ return true;
112
+ }
113
+
91
114
  /**
92
115
  * Concatenate consecutive reasoning blocks in content array.
93
116
  */
@@ -96,48 +119,57 @@ export function concatenateLangchainReasoningBlocks(
96
119
  ): Array<MessageContentComplex | MessageContentReasoningBlock> {
97
120
  const result: Array<MessageContentComplex | MessageContentReasoningBlock> =
98
121
  [];
122
+ let pendingReasoning: MessageContentReasoningBlock | undefined;
123
+
124
+ const flushPendingReasoning = (): void => {
125
+ if (pendingReasoning != null) {
126
+ result.push(pendingReasoning);
127
+ pendingReasoning = undefined;
128
+ }
129
+ };
99
130
 
100
131
  for (const block of content) {
101
- if (block.type === 'reasoning_content') {
102
- const currentReasoning = block as MessageContentReasoningBlock;
103
- const lastIndex = result.length - 1;
104
-
105
- // Check if we can merge with the previous block
106
- if (lastIndex >= 0) {
107
- const lastBlock = result[lastIndex];
108
- if (
109
- lastBlock.type === 'reasoning_content' &&
110
- (lastBlock as MessageContentReasoningBlock).reasoningText != null &&
111
- currentReasoning.reasoningText != null
112
- ) {
113
- const lastReasoning = lastBlock as MessageContentReasoningBlock;
114
- // Merge consecutive reasoning text blocks
115
- const lastText = lastReasoning.reasoningText?.text;
116
- const currentText = currentReasoning.reasoningText.text;
117
- if (
118
- lastText != null &&
119
- lastText !== '' &&
120
- currentText != null &&
121
- currentText !== ''
122
- ) {
123
- lastReasoning.reasoningText!.text = lastText + currentText;
124
- } else if (
125
- currentReasoning.reasoningText.signature != null &&
126
- currentReasoning.reasoningText.signature !== ''
127
- ) {
128
- lastReasoning.reasoningText!.signature =
129
- currentReasoning.reasoningText.signature;
130
- }
131
- continue;
132
- }
132
+ if (block.type !== 'reasoning_content') {
133
+ flushPendingReasoning();
134
+ result.push(block);
135
+ continue;
136
+ }
137
+
138
+ const currentReasoning = block as MessageContentReasoningBlock;
139
+ if (currentReasoning.reasoningText != null) {
140
+ const previousText = pendingReasoning?.reasoningText?.text;
141
+ const previousSignature = pendingReasoning?.reasoningText?.signature;
142
+ const { text, signature } = currentReasoning.reasoningText;
143
+ const mergedReasoningText: { text?: string; signature?: string } = {};
144
+ if (previousText !== undefined || text !== undefined) {
145
+ mergedReasoningText.text = (previousText ?? '') + (text ?? '');
146
+ }
147
+ if (previousSignature !== undefined || signature !== undefined) {
148
+ mergedReasoningText.signature =
149
+ (previousSignature ?? '') + (signature ?? '');
133
150
  }
151
+ pendingReasoning = {
152
+ type: 'reasoning_content',
153
+ reasoningText: mergedReasoningText,
154
+ };
134
155
 
135
- result.push({ ...block } as MessageContentReasoningBlock);
136
- } else {
137
- result.push(block);
156
+ // A signature seals the accumulated reasoning text. Any following
157
+ // reasoning block starts a new independently signed Bedrock block.
158
+ if ('signature' in currentReasoning.reasoningText) {
159
+ flushPendingReasoning();
160
+ }
161
+ }
162
+
163
+ if (currentReasoning.redactedContent != null) {
164
+ flushPendingReasoning();
165
+ result.push({
166
+ type: 'reasoning_content',
167
+ redactedContent: currentReasoning.redactedContent,
168
+ });
138
169
  }
139
170
  }
140
171
 
172
+ flushPendingReasoning();
141
173
  return result;
142
174
  }
143
175
 
@@ -666,23 +698,7 @@ function convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
666
698
  concatenatedBlocks.forEach((block) => {
667
699
  if (block.type === 'text') {
668
700
  const text = (block as { text?: string }).text ?? '';
669
- // Skip completely empty text blocks (common in AI messages with tool_use blocks)
670
- if (text === '') {
671
- return;
672
- }
673
- // Merge whitespace/newlines with previous text blocks to avoid validation errors.
674
- const cleanedText = text.replace(/\n/g, '').trim();
675
- if (cleanedText === '') {
676
- if (contentBlocks.length > 0) {
677
- const lastBlock = contentBlocks[contentBlocks.length - 1];
678
- if ('text' in lastBlock) {
679
- const mergedTextContent = `${lastBlock.text}${text}`;
680
- (lastBlock as { text: string }).text = mergedTextContent;
681
- }
682
- }
683
- } else {
684
- contentBlocks.push({ text });
685
- }
701
+ appendSerializableBedrockTextBlock(contentBlocks, text);
686
702
  } else if (block.type === 'reasoning_content') {
687
703
  const reasoningBlock = block as MessageContentReasoningBlock;
688
704
  // Bedrock Converse rejects reasoningContent whose reasoningText.text is
@@ -757,26 +773,35 @@ function convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
757
773
  function convertFromV1ToChatBedrockConverseMessage(
758
774
  msg: BaseMessage
759
775
  ): BedrockMessage {
776
+ const contentBlocks: BedrockContentBlock[] = [];
760
777
  const assistantMsg: BedrockMessage = {
761
778
  role: 'assistant',
762
- content: [],
779
+ content: contentBlocks,
763
780
  };
781
+ let droppedUnserializableContent = false;
782
+ let unconvertedContentType: string | undefined;
764
783
 
765
784
  if (Array.isArray(msg.content)) {
766
- for (const block of msg.content as Array<
767
- MessageContentComplex | MessageContentReasoningBlock
768
- >) {
785
+ const concatenatedBlocks = concatenateLangchainReasoningBlocks(
786
+ msg.content as Array<MessageContentComplex | MessageContentReasoningBlock>
787
+ );
788
+ for (const block of concatenatedBlocks) {
769
789
  if (typeof block === 'string') {
770
- assistantMsg.content?.push({ text: block });
790
+ if (!appendSerializableBedrockTextBlock(contentBlocks, block)) {
791
+ droppedUnserializableContent = true;
792
+ }
771
793
  } else if (block.type === 'text') {
772
- assistantMsg.content?.push({ text: (block as { text: string }).text });
794
+ const text = (block as { text?: string }).text ?? '';
795
+ if (!appendSerializableBedrockTextBlock(contentBlocks, text)) {
796
+ droppedUnserializableContent = true;
797
+ }
773
798
  } else if (block.type === 'tool_call') {
774
799
  const toolCall = block as {
775
800
  id: string;
776
801
  name: string;
777
802
  args: Record<string, unknown>;
778
803
  };
779
- assistantMsg.content?.push({
804
+ contentBlocks.push({
780
805
  toolUse: {
781
806
  toolUseId: toolCall.id,
782
807
  name: toolCall.name,
@@ -784,38 +809,51 @@ function convertFromV1ToChatBedrockConverseMessage(
784
809
  },
785
810
  } as BedrockContentBlock);
786
811
  } else if (block.type === 'reasoning') {
787
- const reasoning = block as { reasoning: string };
788
- assistantMsg.content?.push({
812
+ const reasoning = block as { reasoning?: string };
813
+ /** Bedrock Converse rejects `reasoningText` with a null/empty `text`
814
+ * (`Member must not be null`), e.g. when the producing model omitted
815
+ * reasoning text (`thinking.display: "omitted"`) — drop rather than send. */
816
+ if (reasoning.reasoning == null || reasoning.reasoning === '') {
817
+ droppedUnserializableContent = true;
818
+ continue;
819
+ }
820
+ contentBlocks.push({
789
821
  reasoningContent: {
790
822
  reasoningText: { text: reasoning.reasoning },
791
823
  },
792
824
  } as BedrockContentBlock);
793
825
  } else if (block.type === 'reasoning_content') {
794
- assistantMsg.content?.push({
795
- reasoningContent: langchainReasoningBlockToBedrockReasoningBlock(
796
- block as MessageContentReasoningBlock
797
- ),
826
+ const reasoningBlock = block as MessageContentReasoningBlock;
827
+ if (!isSerializableBedrockReasoningBlock(reasoningBlock)) {
828
+ droppedUnserializableContent = true;
829
+ continue;
830
+ }
831
+ contentBlocks.push({
832
+ reasoningContent:
833
+ langchainReasoningBlockToBedrockReasoningBlock(reasoningBlock),
798
834
  } as BedrockContentBlock);
835
+ } else {
836
+ unconvertedContentType ??= block.type;
799
837
  }
800
838
  }
801
- } else if (typeof msg.content === 'string' && msg.content !== '') {
802
- assistantMsg.content?.push({ text: msg.content });
839
+ } else if (typeof msg.content === 'string') {
840
+ if (!appendSerializableBedrockTextBlock(contentBlocks, msg.content)) {
841
+ droppedUnserializableContent = true;
842
+ }
803
843
  }
804
844
 
805
845
  // Also handle tool_calls from the message
806
846
  if (isAIMessage(msg) && msg.tool_calls != null && msg.tool_calls.length > 0) {
807
847
  // Check if tool calls are already in content
808
848
  const existingToolUseIds = new Set(
809
- assistantMsg.content
810
- ?.filter((c) => 'toolUse' in c)
811
- .map(
812
- (c) => (c as { toolUse: { toolUseId: string } }).toolUse.toolUseId
813
- ) ?? []
849
+ contentBlocks
850
+ .filter((c) => 'toolUse' in c)
851
+ .map((c) => (c as { toolUse: { toolUseId: string } }).toolUse.toolUseId)
814
852
  );
815
853
 
816
854
  for (const tc of msg.tool_calls) {
817
855
  if (!existingToolUseIds.has(tc.id ?? '')) {
818
- assistantMsg.content?.push({
856
+ contentBlocks.push({
819
857
  toolUse: {
820
858
  toolUseId: tc.id,
821
859
  name: tc.name,
@@ -826,6 +864,16 @@ function convertFromV1ToChatBedrockConverseMessage(
826
864
  }
827
865
  }
828
866
 
867
+ const hasNoContent = contentBlocks.length === 0;
868
+ if (hasNoContent && unconvertedContentType != null) {
869
+ throw new Error(
870
+ `Unsupported v1 content block type: ${unconvertedContentType}`
871
+ );
872
+ }
873
+ if (hasNoContent && droppedUnserializableContent) {
874
+ contentBlocks.push({ text: BEDROCK_EMPTY_TEXT_PLACEHOLDER });
875
+ }
876
+
829
877
  return assistantMsg;
830
878
  }
831
879