@librechat/agents 3.0.79 → 3.0.81

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.
@@ -21,17 +21,6 @@ import { ChatGenerationChunk, ChatResult } from '@langchain/core/outputs';
21
21
  import type { CallbackManagerForLLMRun } from '@langchain/core/callbacks/manager';
22
22
  import type { ChatBedrockConverseInput } from '@langchain/aws';
23
23
  import type { BaseMessage } from '@langchain/core/messages';
24
- import {
25
- ConverseCommand,
26
- ConverseStreamCommand,
27
- } from '@aws-sdk/client-bedrock-runtime';
28
- import {
29
- convertToConverseMessages,
30
- convertConverseMessageToLangChainMessage,
31
- handleConverseStreamContentBlockStart,
32
- handleConverseStreamContentBlockDelta,
33
- handleConverseStreamMetadata,
34
- } from './utils';
35
24
 
36
25
  /**
37
26
  * Service tier type for Bedrock invocations.
@@ -119,7 +108,7 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
119
108
  } {
120
109
  const baseParams = super.invocationParams(options);
121
110
 
122
- // Get serviceTier from options or fall back to class-level setting
111
+ /** Service tier from options or fall back to class-level setting */
123
112
  const serviceTierType = options?.serviceTier ?? this.serviceTier;
124
113
 
125
114
  return {
@@ -130,110 +119,67 @@ export class CustomChatBedrockConverse extends ChatBedrockConverse {
130
119
 
131
120
  /**
132
121
  * Override _generateNonStreaming to use applicationInferenceProfile as modelId.
122
+ * Uses the same model-swapping pattern as streaming for consistency.
133
123
  */
134
124
  override async _generateNonStreaming(
135
125
  messages: BaseMessage[],
136
126
  options: this['ParsedCallOptions'] & CustomChatBedrockConverseCallOptions,
137
- _runManager?: CallbackManagerForLLMRun
127
+ runManager?: CallbackManagerForLLMRun
138
128
  ): Promise<ChatResult> {
139
- const { converseMessages, converseSystem } =
140
- convertToConverseMessages(messages);
141
- const params = this.invocationParams(options);
142
-
143
- const command = new ConverseCommand({
144
- modelId: this.getModelId(),
145
- messages: converseMessages,
146
- system: converseSystem,
147
- requestMetadata: options.requestMetadata,
148
- ...params,
149
- });
150
-
151
- const response = await this.client.send(command, {
152
- abortSignal: options.signal,
153
- });
154
-
155
- const { output, ...responseMetadata } = response;
156
- if (!output?.message) {
157
- throw new Error('No message found in Bedrock response.');
129
+ // Temporarily swap model for applicationInferenceProfile support
130
+ const originalModel = this.model;
131
+ if (
132
+ this.applicationInferenceProfile != null &&
133
+ this.applicationInferenceProfile !== ''
134
+ ) {
135
+ this.model = this.applicationInferenceProfile;
158
136
  }
159
137
 
160
- const message = convertConverseMessageToLangChainMessage(
161
- output.message,
162
- responseMetadata
163
- );
164
-
165
- return {
166
- generations: [
167
- {
168
- text: typeof message.content === 'string' ? message.content : '',
169
- message,
170
- },
171
- ],
172
- };
138
+ try {
139
+ return await super._generateNonStreaming(messages, options, runManager);
140
+ } finally {
141
+ // Restore original model
142
+ this.model = originalModel;
143
+ }
173
144
  }
174
145
 
175
146
  /**
176
147
  * Override _streamResponseChunks to:
177
- * 1. Use applicationInferenceProfile as modelId
178
- * 2. Include serviceTier in request
179
- * 3. Strip contentBlockIndex from response_metadata to prevent merge conflicts
148
+ * 1. Use applicationInferenceProfile as modelId (by temporarily swapping this.model)
149
+ * 2. Strip contentBlockIndex from response_metadata to prevent merge conflicts
150
+ *
151
+ * Note: We delegate to super._streamResponseChunks() to preserve @langchain/aws's
152
+ * internal chunk handling which correctly preserves array content for reasoning blocks.
180
153
  */
181
154
  override async *_streamResponseChunks(
182
155
  messages: BaseMessage[],
183
156
  options: this['ParsedCallOptions'] & CustomChatBedrockConverseCallOptions,
184
157
  runManager?: CallbackManagerForLLMRun
185
158
  ): AsyncGenerator<ChatGenerationChunk> {
186
- const { converseMessages, converseSystem } =
187
- convertToConverseMessages(messages);
188
- const params = this.invocationParams(options);
189
-
190
- let { streamUsage } = this;
191
- if (options.streamUsage !== undefined) {
192
- streamUsage = options.streamUsage;
159
+ // Temporarily swap model for applicationInferenceProfile support
160
+ const originalModel = this.model;
161
+ if (
162
+ this.applicationInferenceProfile != null &&
163
+ this.applicationInferenceProfile !== ''
164
+ ) {
165
+ this.model = this.applicationInferenceProfile;
193
166
  }
194
167
 
195
- const command = new ConverseStreamCommand({
196
- modelId: this.getModelId(),
197
- messages: converseMessages,
198
- system: converseSystem,
199
- requestMetadata: options.requestMetadata,
200
- ...params,
201
- });
202
-
203
- const response = await this.client.send(command, {
204
- abortSignal: options.signal,
205
- });
206
-
207
- if (response.stream) {
208
- for await (const event of response.stream) {
209
- if (event.contentBlockStart != null) {
210
- const chunk = handleConverseStreamContentBlockStart(
211
- event.contentBlockStart
212
- ) as ChatGenerationChunk | undefined;
213
- if (chunk !== undefined) {
214
- const cleanedChunk = this.cleanChunk(chunk);
215
- yield cleanedChunk;
216
- await runManager?.handleLLMNewToken(cleanedChunk.text || '');
217
- }
218
- } else if (event.contentBlockDelta != null) {
219
- const chunk = handleConverseStreamContentBlockDelta(
220
- event.contentBlockDelta
221
- ) as ChatGenerationChunk | undefined;
222
- if (chunk !== undefined) {
223
- const cleanedChunk = this.cleanChunk(chunk);
224
- yield cleanedChunk;
225
- await runManager?.handleLLMNewToken(cleanedChunk.text || '');
226
- }
227
- } else if (event.metadata != null) {
228
- const chunk = handleConverseStreamMetadata(event.metadata, {
229
- streamUsage,
230
- }) as ChatGenerationChunk | undefined;
231
- if (chunk !== undefined) {
232
- const cleanedChunk = this.cleanChunk(chunk);
233
- yield cleanedChunk;
234
- }
235
- }
168
+ try {
169
+ // Use parent's streaming logic which correctly handles reasoning content
170
+ const baseStream = super._streamResponseChunks(
171
+ messages,
172
+ options,
173
+ runManager
174
+ );
175
+
176
+ for await (const chunk of baseStream) {
177
+ // Clean contentBlockIndex from response_metadata to prevent merge conflicts
178
+ yield this.cleanChunk(chunk);
236
179
  }
180
+ } finally {
181
+ // Restore original model
182
+ this.model = originalModel;
237
183
  }
238
184
  }
239
185
 
@@ -38,13 +38,13 @@ export function bedrockReasoningDeltaToLangchainPartialReasoningBlock(
38
38
  reasoningText: { text },
39
39
  };
40
40
  }
41
- if (signature) {
41
+ if (signature != null) {
42
42
  return {
43
43
  type: 'reasoning_content',
44
44
  reasoningText: { signature },
45
45
  };
46
46
  }
47
- if (redactedContent) {
47
+ if (redactedContent != null) {
48
48
  return {
49
49
  type: 'reasoning_content',
50
50
  redactedContent: Buffer.from(redactedContent).toString('base64'),
@@ -65,13 +65,13 @@ export function bedrockReasoningBlockToLangchainReasoningBlock(
65
65
  redactedContent?: Uint8Array;
66
66
  };
67
67
 
68
- if (reasoningText) {
68
+ if (reasoningText != null) {
69
69
  return {
70
70
  type: 'reasoning_content',
71
71
  reasoningText: reasoningText,
72
72
  };
73
73
  }
74
- if (redactedContent) {
74
+ if (redactedContent != null) {
75
75
  return {
76
76
  type: 'reasoning_content',
77
77
  redactedContent: Buffer.from(redactedContent).toString('base64'),
@@ -87,7 +87,7 @@ export function convertConverseMessageToLangChainMessage(
87
87
  message: BedrockMessage,
88
88
  responseMetadata: Omit<ConverseResponse, 'output'>
89
89
  ): AIMessage {
90
- if (!message.content) {
90
+ if (message.content == null) {
91
91
  throw new Error('No message content found in response.');
92
92
  }
93
93
  if (message.role !== 'assistant') {
@@ -99,7 +99,7 @@ export function convertConverseMessageToLangChainMessage(
99
99
  let requestId: string | undefined;
100
100
  if (
101
101
  '$metadata' in responseMetadata &&
102
- responseMetadata.$metadata &&
102
+ responseMetadata.$metadata != null &&
103
103
  typeof responseMetadata.$metadata === 'object' &&
104
104
  'requestId' in responseMetadata.$metadata
105
105
  ) {
@@ -109,7 +109,7 @@ export function convertConverseMessageToLangChainMessage(
109
109
  let tokenUsage:
110
110
  | { input_tokens: number; output_tokens: number; total_tokens: number }
111
111
  | undefined;
112
- if (responseMetadata.usage) {
112
+ if (responseMetadata.usage != null) {
113
113
  const input_tokens = responseMetadata.usage.inputTokens ?? 0;
114
114
  const output_tokens = responseMetadata.usage.outputTokens ?? 0;
115
115
  tokenUsage = {
@@ -144,9 +144,10 @@ export function convertConverseMessageToLangChainMessage(
144
144
  message.content.forEach((c) => {
145
145
  if (
146
146
  'toolUse' in c &&
147
- c.toolUse &&
148
- c.toolUse.name &&
149
- c.toolUse.input &&
147
+ c.toolUse != null &&
148
+ c.toolUse.name != null &&
149
+ c.toolUse.name !== '' &&
150
+ c.toolUse.input != null &&
150
151
  typeof c.toolUse.input === 'object'
151
152
  ) {
152
153
  toolCalls.push({
@@ -157,7 +158,7 @@ export function convertConverseMessageToLangChainMessage(
157
158
  });
158
159
  } else if ('text' in c && typeof c.text === 'string') {
159
160
  content.push({ type: 'text', text: c.text });
160
- } else if ('reasoningContent' in c && c.reasoningContent) {
161
+ } else if ('reasoningContent' in c && c.reasoningContent != null) {
161
162
  content.push(
162
163
  bedrockReasoningBlockToLangchainReasoningBlock(c.reasoningContent)
163
164
  );
@@ -182,7 +183,7 @@ export function convertConverseMessageToLangChainMessage(
182
183
  export function handleConverseStreamContentBlockDelta(
183
184
  contentBlockDelta: ContentBlockDeltaEvent
184
185
  ): ChatGenerationChunk {
185
- if (!contentBlockDelta.delta) {
186
+ if (contentBlockDelta.delta == null) {
186
187
  throw new Error('No delta found in content block.');
187
188
  }
188
189
 
@@ -196,7 +197,7 @@ export function handleConverseStreamContentBlockDelta(
196
197
  },
197
198
  }),
198
199
  });
199
- } else if (contentBlockDelta.delta.toolUse) {
200
+ } else if (contentBlockDelta.delta.toolUse != null) {
200
201
  const index = contentBlockDelta.contentBlockIndex;
201
202
  return new ChatGenerationChunk({
202
203
  text: '',
@@ -214,15 +215,28 @@ export function handleConverseStreamContentBlockDelta(
214
215
  },
215
216
  }),
216
217
  });
217
- } else if (contentBlockDelta.delta.reasoningContent) {
218
+ } else if (contentBlockDelta.delta.reasoningContent != null) {
219
+ const reasoningBlock =
220
+ bedrockReasoningDeltaToLangchainPartialReasoningBlock(
221
+ contentBlockDelta.delta.reasoningContent
222
+ );
223
+ // Extract the text for additional_kwargs.reasoning_content (for stream handler compatibility)
224
+ const reasoningText =
225
+ 'reasoningText' in reasoningBlock
226
+ ? (reasoningBlock.reasoningText.text ??
227
+ reasoningBlock.reasoningText.signature ??
228
+ ('redactedContent' in reasoningBlock
229
+ ? reasoningBlock.redactedContent
230
+ : ''))
231
+ : '';
218
232
  return new ChatGenerationChunk({
219
233
  text: '',
220
234
  message: new AIMessageChunk({
221
- content: [
222
- bedrockReasoningDeltaToLangchainPartialReasoningBlock(
223
- contentBlockDelta.delta.reasoningContent
224
- ),
225
- ],
235
+ content: [reasoningBlock],
236
+ additional_kwargs: {
237
+ // Set reasoning_content for stream handler to detect reasoning mode
238
+ reasoning_content: reasoningText,
239
+ },
226
240
  response_metadata: {
227
241
  contentBlockIndex: contentBlockDelta.contentBlockIndex,
228
242
  },
@@ -243,7 +257,7 @@ export function handleConverseStreamContentBlockStart(
243
257
  ): ChatGenerationChunk | null {
244
258
  const index = contentBlockStart.contentBlockIndex;
245
259
 
246
- if (contentBlockStart.start?.toolUse) {
260
+ if (contentBlockStart.start?.toolUse != null) {
247
261
  return new ChatGenerationChunk({
248
262
  text: '',
249
263
  message: new AIMessageChunk({
@@ -835,6 +835,221 @@ describe('Multi-agent provider interoperability', () => {
835
835
  });
836
836
  });
837
837
 
838
+ describe('Immutability - addCacheControl does not mutate original messages', () => {
839
+ it('should not mutate original messages when adding cache control to string content', () => {
840
+ const originalMessages: TestMsg[] = [
841
+ { role: 'user', content: 'Hello' },
842
+ { role: 'assistant', content: 'Hi there' },
843
+ { role: 'user', content: 'How are you?' },
844
+ ];
845
+
846
+ const originalFirstContent = originalMessages[0].content;
847
+ const originalThirdContent = originalMessages[2].content;
848
+
849
+ const result = addCacheControl(originalMessages as never);
850
+
851
+ expect(originalMessages[0].content).toBe(originalFirstContent);
852
+ expect(originalMessages[2].content).toBe(originalThirdContent);
853
+ expect(typeof originalMessages[0].content).toBe('string');
854
+ expect(typeof originalMessages[2].content).toBe('string');
855
+
856
+ expect(Array.isArray(result[0].content)).toBe(true);
857
+ expect(Array.isArray(result[2].content)).toBe(true);
858
+ });
859
+
860
+ it('should not mutate original messages when adding cache control to array content', () => {
861
+ const originalMessages: TestMsg[] = [
862
+ {
863
+ role: 'user',
864
+ content: [{ type: ContentTypes.TEXT, text: 'Hello' }],
865
+ },
866
+ { role: 'assistant', content: 'Hi there' },
867
+ {
868
+ role: 'user',
869
+ content: [{ type: ContentTypes.TEXT, text: 'How are you?' }],
870
+ },
871
+ ];
872
+
873
+ const originalFirstBlock = {
874
+ ...(originalMessages[0].content as MessageContentComplex[])[0],
875
+ };
876
+ const originalThirdBlock = {
877
+ ...(originalMessages[2].content as MessageContentComplex[])[0],
878
+ };
879
+
880
+ const result = addCacheControl(originalMessages as never);
881
+
882
+ const firstContent = originalMessages[0].content as MessageContentComplex[];
883
+ const thirdContent = originalMessages[2].content as MessageContentComplex[];
884
+
885
+ expect('cache_control' in firstContent[0]).toBe(false);
886
+ expect('cache_control' in thirdContent[0]).toBe(false);
887
+ expect(firstContent[0]).toEqual(originalFirstBlock);
888
+ expect(thirdContent[0]).toEqual(originalThirdBlock);
889
+
890
+ const resultFirstContent = result[0].content as MessageContentComplex[];
891
+ const resultThirdContent = result[2].content as MessageContentComplex[];
892
+ expect('cache_control' in resultFirstContent[0]).toBe(true);
893
+ expect('cache_control' in resultThirdContent[0]).toBe(true);
894
+ });
895
+
896
+ it('should not mutate original messages when stripping existing cache control', () => {
897
+ const originalMessages: TestMsg[] = [
898
+ {
899
+ role: 'user',
900
+ content: [
901
+ {
902
+ type: ContentTypes.TEXT,
903
+ text: 'Hello',
904
+ cache_control: { type: 'ephemeral' },
905
+ } as MessageContentComplex,
906
+ ],
907
+ },
908
+ { role: 'assistant', content: 'Hi there' },
909
+ {
910
+ role: 'user',
911
+ content: [{ type: ContentTypes.TEXT, text: 'How are you?' }],
912
+ },
913
+ ];
914
+
915
+ const originalFirstBlock = (
916
+ originalMessages[0].content as MessageContentComplex[]
917
+ )[0];
918
+
919
+ addCacheControl(originalMessages as never);
920
+
921
+ expect('cache_control' in originalFirstBlock).toBe(true);
922
+ });
923
+ });
924
+
925
+ describe('Immutability - addBedrockCacheControl does not mutate original messages', () => {
926
+ it('should not mutate original messages when adding cache points to string content', () => {
927
+ const originalMessages: TestMsg[] = [
928
+ { role: 'user', content: 'Hello' },
929
+ { role: 'assistant', content: 'Hi there' },
930
+ ];
931
+
932
+ const originalFirstContent = originalMessages[0].content;
933
+ const originalSecondContent = originalMessages[1].content;
934
+
935
+ const result = addBedrockCacheControl(originalMessages);
936
+
937
+ expect(originalMessages[0].content).toBe(originalFirstContent);
938
+ expect(originalMessages[1].content).toBe(originalSecondContent);
939
+ expect(typeof originalMessages[0].content).toBe('string');
940
+ expect(typeof originalMessages[1].content).toBe('string');
941
+
942
+ expect(Array.isArray(result[0].content)).toBe(true);
943
+ expect(Array.isArray(result[1].content)).toBe(true);
944
+ });
945
+
946
+ it('should not mutate original messages when adding cache points to array content', () => {
947
+ const originalMessages: TestMsg[] = [
948
+ {
949
+ role: 'user',
950
+ content: [{ type: ContentTypes.TEXT, text: 'Hello' }],
951
+ },
952
+ {
953
+ role: 'assistant',
954
+ content: [{ type: ContentTypes.TEXT, text: 'Hi there' }],
955
+ },
956
+ ];
957
+
958
+ const originalFirstContentLength = (
959
+ originalMessages[0].content as MessageContentComplex[]
960
+ ).length;
961
+ const originalSecondContentLength = (
962
+ originalMessages[1].content as MessageContentComplex[]
963
+ ).length;
964
+
965
+ const result = addBedrockCacheControl(originalMessages);
966
+
967
+ const firstContent = originalMessages[0].content as MessageContentComplex[];
968
+ const secondContent = originalMessages[1]
969
+ .content as MessageContentComplex[];
970
+
971
+ expect(firstContent.length).toBe(originalFirstContentLength);
972
+ expect(secondContent.length).toBe(originalSecondContentLength);
973
+ expect(firstContent.some((b) => 'cachePoint' in b)).toBe(false);
974
+ expect(secondContent.some((b) => 'cachePoint' in b)).toBe(false);
975
+
976
+ const resultFirstContent = result[0].content as MessageContentComplex[];
977
+ const resultSecondContent = result[1].content as MessageContentComplex[];
978
+ expect(resultFirstContent.length).toBe(originalFirstContentLength + 1);
979
+ expect(resultSecondContent.length).toBe(originalSecondContentLength + 1);
980
+ expect(resultFirstContent.some((b) => 'cachePoint' in b)).toBe(true);
981
+ expect(resultSecondContent.some((b) => 'cachePoint' in b)).toBe(true);
982
+ });
983
+
984
+ it('should not mutate original messages when stripping existing cache control', () => {
985
+ const originalMessages: TestMsg[] = [
986
+ {
987
+ role: 'user',
988
+ content: [
989
+ {
990
+ type: ContentTypes.TEXT,
991
+ text: 'Hello',
992
+ cache_control: { type: 'ephemeral' },
993
+ } as MessageContentComplex,
994
+ ],
995
+ },
996
+ {
997
+ role: 'assistant',
998
+ content: [
999
+ { type: ContentTypes.TEXT, text: 'Hi there' },
1000
+ { cachePoint: { type: 'default' } },
1001
+ ],
1002
+ },
1003
+ ];
1004
+
1005
+ const originalFirstBlock = (
1006
+ originalMessages[0].content as MessageContentComplex[]
1007
+ )[0];
1008
+ const originalSecondContentLength = (
1009
+ originalMessages[1].content as MessageContentComplex[]
1010
+ ).length;
1011
+
1012
+ addBedrockCacheControl(originalMessages);
1013
+
1014
+ expect('cache_control' in originalFirstBlock).toBe(true);
1015
+ expect(
1016
+ (originalMessages[1].content as MessageContentComplex[]).length
1017
+ ).toBe(originalSecondContentLength);
1018
+ });
1019
+
1020
+ it('should allow different providers to process same messages without cross-contamination', () => {
1021
+ const sharedMessages: TestMsg[] = [
1022
+ {
1023
+ role: 'user',
1024
+ content: [{ type: ContentTypes.TEXT, text: 'Shared message 1' }],
1025
+ },
1026
+ {
1027
+ role: 'assistant',
1028
+ content: [{ type: ContentTypes.TEXT, text: 'Shared response 1' }],
1029
+ },
1030
+ ];
1031
+
1032
+ const bedrockResult = addBedrockCacheControl(sharedMessages);
1033
+
1034
+ const anthropicResult = addCacheControl(sharedMessages as never);
1035
+
1036
+ const originalFirstContent = sharedMessages[0]
1037
+ .content as MessageContentComplex[];
1038
+ expect(originalFirstContent.some((b) => 'cachePoint' in b)).toBe(false);
1039
+ expect('cache_control' in originalFirstContent[0]).toBe(false);
1040
+
1041
+ const bedrockFirstContent = bedrockResult[0]
1042
+ .content as MessageContentComplex[];
1043
+ expect(bedrockFirstContent.some((b) => 'cachePoint' in b)).toBe(true);
1044
+ expect('cache_control' in bedrockFirstContent[0]).toBe(false);
1045
+
1046
+ const anthropicFirstContent = anthropicResult[0]
1047
+ .content as MessageContentComplex[];
1048
+ expect(anthropicFirstContent.some((b) => 'cachePoint' in b)).toBe(false);
1049
+ expect('cache_control' in anthropicFirstContent[0]).toBe(true);
1050
+ });
1051
+ });
1052
+
838
1053
  describe('Multi-turn cache cleanup', () => {
839
1054
  it('strips stale Bedrock cache points from previous turns before applying new ones', () => {
840
1055
  const messages: TestMsg[] = [