@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.
@@ -7,13 +7,57 @@ type MessageWithContent = {
7
7
  content?: string | MessageContentComplex[];
8
8
  };
9
9
 
10
+ /**
11
+ * Deep clones a message's content to prevent mutation of the original.
12
+ * Handles both string and array content types.
13
+ */
14
+ function deepCloneContent<T extends string | MessageContentComplex[]>(
15
+ content: T
16
+ ): T {
17
+ if (typeof content === 'string') {
18
+ return content;
19
+ }
20
+ if (Array.isArray(content)) {
21
+ return content.map((block) => ({ ...block })) as T;
22
+ }
23
+ return content;
24
+ }
25
+
26
+ /**
27
+ * Creates a shallow clone of a message with deep-cloned content.
28
+ * This ensures modifications to content don't affect the original message.
29
+ */
30
+ function cloneMessageWithContent<T extends MessageWithContent>(message: T): T {
31
+ if (message.content === undefined) {
32
+ return { ...message };
33
+ }
34
+ return {
35
+ ...message,
36
+ content: deepCloneContent(message.content),
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Checks if a message's content needs cache control stripping.
42
+ * Returns true if content has cachePoint blocks or cache_control fields.
43
+ */
44
+ function needsCacheStripping(content: MessageContentComplex[]): boolean {
45
+ for (let i = 0; i < content.length; i++) {
46
+ const block = content[i];
47
+ if (isCachePoint(block)) return true;
48
+ if ('cache_control' in block) return true;
49
+ }
50
+ return false;
51
+ }
52
+
10
53
  /**
11
54
  * Anthropic API: Adds cache control to the appropriate user messages in the payload.
12
55
  * Strips ALL existing cache control (both Anthropic and Bedrock formats) from all messages,
13
56
  * then adds fresh cache control to the last 2 user messages in a single backward pass.
14
57
  * This ensures we don't accumulate stale cache points across multiple turns.
58
+ * Returns a new array - only clones messages that require modification.
15
59
  * @param messages - The array of message objects.
16
- * @returns - The updated array of message objects with cache control added.
60
+ * @returns - A new array of message objects with cache control added.
17
61
  */
18
62
  export function addCacheControl<T extends AnthropicMessage | BaseMessage>(
19
63
  messages: T[]
@@ -22,22 +66,47 @@ export function addCacheControl<T extends AnthropicMessage | BaseMessage>(
22
66
  return messages;
23
67
  }
24
68
 
25
- const updatedMessages = [...messages];
69
+ const updatedMessages: T[] = [...messages];
26
70
  let userMessagesModified = 0;
27
71
 
28
72
  for (let i = updatedMessages.length - 1; i >= 0; i--) {
29
- const message = updatedMessages[i];
73
+ const originalMessage = updatedMessages[i];
30
74
  const isUserMessage =
31
- ('getType' in message && message.getType() === 'human') ||
32
- ('role' in message && message.role === 'user');
75
+ ('getType' in originalMessage && originalMessage.getType() === 'human') ||
76
+ ('role' in originalMessage && originalMessage.role === 'user');
77
+
78
+ const hasArrayContent = Array.isArray(originalMessage.content);
79
+ const needsStripping =
80
+ hasArrayContent &&
81
+ needsCacheStripping(originalMessage.content as MessageContentComplex[]);
82
+ const needsCacheAdd =
83
+ userMessagesModified < 2 &&
84
+ isUserMessage &&
85
+ (typeof originalMessage.content === 'string' || hasArrayContent);
86
+
87
+ if (!needsStripping && !needsCacheAdd) {
88
+ continue;
89
+ }
33
90
 
34
- if (Array.isArray(message.content)) {
35
- message.content = message.content.filter(
91
+ const message = cloneMessageWithContent(
92
+ originalMessage as MessageWithContent
93
+ ) as T;
94
+ updatedMessages[i] = message;
95
+
96
+ if (hasArrayContent) {
97
+ message.content = (message.content as MessageContentComplex[]).filter(
36
98
  (block) => !isCachePoint(block as MessageContentComplex)
37
99
  ) as typeof message.content;
38
100
 
39
- for (let j = 0; j < message.content.length; j++) {
40
- const block = message.content[j] as Record<string, unknown>;
101
+ for (
102
+ let j = 0;
103
+ j < (message.content as MessageContentComplex[]).length;
104
+ j++
105
+ ) {
106
+ const block = (message.content as MessageContentComplex[])[j] as Record<
107
+ string,
108
+ unknown
109
+ >;
41
110
  if ('cache_control' in block) {
42
111
  delete block.cache_control;
43
112
  }
@@ -81,9 +150,20 @@ function isCachePoint(block: MessageContentComplex): boolean {
81
150
  return 'cachePoint' in block && !('type' in block);
82
151
  }
83
152
 
153
+ /**
154
+ * Checks if a message's content has Anthropic cache_control fields.
155
+ */
156
+ function hasAnthropicCacheControl(content: MessageContentComplex[]): boolean {
157
+ for (let i = 0; i < content.length; i++) {
158
+ if ('cache_control' in content[i]) return true;
159
+ }
160
+ return false;
161
+ }
162
+
84
163
  /**
85
164
  * Removes all Anthropic cache_control fields from messages
86
165
  * Used when switching from Anthropic to Bedrock provider
166
+ * Returns a new array - only clones messages that require modification.
87
167
  */
88
168
  export function stripAnthropicCacheControl<T extends MessageWithContent>(
89
169
  messages: T[]
@@ -92,18 +172,30 @@ export function stripAnthropicCacheControl<T extends MessageWithContent>(
92
172
  return messages;
93
173
  }
94
174
 
95
- const updatedMessages = [...messages];
175
+ const updatedMessages: T[] = [...messages];
96
176
 
97
177
  for (let i = 0; i < updatedMessages.length; i++) {
98
- const message = updatedMessages[i];
99
- const content = message.content;
178
+ const originalMessage = updatedMessages[i];
179
+ const content = originalMessage.content;
100
180
 
101
- if (Array.isArray(content)) {
102
- for (let j = 0; j < content.length; j++) {
103
- const block = content[j] as Record<string, unknown>;
104
- if ('cache_control' in block) {
105
- delete block.cache_control;
106
- }
181
+ if (!Array.isArray(content) || !hasAnthropicCacheControl(content)) {
182
+ continue;
183
+ }
184
+
185
+ const message = cloneMessageWithContent(originalMessage);
186
+ updatedMessages[i] = message;
187
+
188
+ for (
189
+ let j = 0;
190
+ j < (message.content as MessageContentComplex[]).length;
191
+ j++
192
+ ) {
193
+ const block = (message.content as MessageContentComplex[])[j] as Record<
194
+ string,
195
+ unknown
196
+ >;
197
+ if ('cache_control' in block) {
198
+ delete block.cache_control;
107
199
  }
108
200
  }
109
201
  }
@@ -111,9 +203,20 @@ export function stripAnthropicCacheControl<T extends MessageWithContent>(
111
203
  return updatedMessages;
112
204
  }
113
205
 
206
+ /**
207
+ * Checks if a message's content has Bedrock cachePoint blocks.
208
+ */
209
+ function hasBedrockCachePoint(content: MessageContentComplex[]): boolean {
210
+ for (let i = 0; i < content.length; i++) {
211
+ if (isCachePoint(content[i])) return true;
212
+ }
213
+ return false;
214
+ }
215
+
114
216
  /**
115
217
  * Removes all Bedrock cachePoint blocks from messages
116
218
  * Used when switching from Bedrock to Anthropic provider
219
+ * Returns a new array - only clones messages that require modification.
117
220
  */
118
221
  export function stripBedrockCacheControl<T extends MessageWithContent>(
119
222
  messages: T[]
@@ -122,17 +225,22 @@ export function stripBedrockCacheControl<T extends MessageWithContent>(
122
225
  return messages;
123
226
  }
124
227
 
125
- const updatedMessages = [...messages];
228
+ const updatedMessages: T[] = [...messages];
126
229
 
127
230
  for (let i = 0; i < updatedMessages.length; i++) {
128
- const message = updatedMessages[i];
129
- const content = message.content;
231
+ const originalMessage = updatedMessages[i];
232
+ const content = originalMessage.content;
130
233
 
131
- if (Array.isArray(content)) {
132
- message.content = content.filter(
133
- (block) => !isCachePoint(block as MessageContentComplex)
134
- ) as typeof content;
234
+ if (!Array.isArray(content) || !hasBedrockCachePoint(content)) {
235
+ continue;
135
236
  }
237
+
238
+ const message = cloneMessageWithContent(originalMessage);
239
+ updatedMessages[i] = message;
240
+
241
+ message.content = (message.content as MessageContentComplex[]).filter(
242
+ (block) => !isCachePoint(block as MessageContentComplex)
243
+ ) as typeof content;
136
244
  }
137
245
 
138
246
  return updatedMessages;
@@ -145,8 +253,9 @@ export function stripBedrockCacheControl<T extends MessageWithContent>(
145
253
  * Strips ALL existing cache control (both Bedrock and Anthropic formats) from all messages,
146
254
  * then adds fresh cache points to the last 2 messages in a single backward pass.
147
255
  * This ensures we don't accumulate stale cache points across multiple turns.
256
+ * Returns a new array - only clones messages that require modification.
148
257
  * @param messages - The array of message objects.
149
- * @returns - The updated array of message objects with cache points added.
258
+ * @returns - A new array of message objects with cache points added.
150
259
  */
151
260
  export function addBedrockCacheControl<
152
261
  T extends Partial<BaseMessage> & MessageWithContent,
@@ -155,42 +264,62 @@ export function addBedrockCacheControl<
155
264
  return messages;
156
265
  }
157
266
 
158
- const updatedMessages: T[] = messages.slice();
267
+ const updatedMessages: T[] = [...messages];
159
268
  let messagesModified = 0;
160
269
 
161
270
  for (let i = updatedMessages.length - 1; i >= 0; i--) {
162
- const message = updatedMessages[i];
271
+ const originalMessage = updatedMessages[i];
163
272
  const isToolMessage =
164
- 'getType' in message &&
165
- typeof message.getType === 'function' &&
166
- message.getType() === 'tool';
273
+ 'getType' in originalMessage &&
274
+ typeof originalMessage.getType === 'function' &&
275
+ originalMessage.getType() === 'tool';
276
+
277
+ const content = originalMessage.content;
278
+ const hasArrayContent = Array.isArray(content);
279
+ const needsStripping =
280
+ hasArrayContent &&
281
+ needsCacheStripping(content as MessageContentComplex[]);
282
+ const isEmptyString = typeof content === 'string' && content === '';
283
+ const needsCacheAdd =
284
+ messagesModified < 2 &&
285
+ !isToolMessage &&
286
+ !isEmptyString &&
287
+ (typeof content === 'string' || hasArrayContent);
288
+
289
+ if (!needsStripping && !needsCacheAdd) {
290
+ continue;
291
+ }
167
292
 
168
- const content = message.content;
293
+ const message = cloneMessageWithContent(originalMessage);
294
+ updatedMessages[i] = message;
169
295
 
170
- if (Array.isArray(content)) {
171
- message.content = content.filter(
296
+ if (hasArrayContent) {
297
+ message.content = (message.content as MessageContentComplex[]).filter(
172
298
  (block) => !isCachePoint(block)
173
299
  ) as typeof content;
174
300
 
175
- for (let j = 0; j < message.content.length; j++) {
176
- const block = message.content[j] as Record<string, unknown>;
301
+ for (
302
+ let j = 0;
303
+ j < (message.content as MessageContentComplex[]).length;
304
+ j++
305
+ ) {
306
+ const block = (message.content as MessageContentComplex[])[j] as Record<
307
+ string,
308
+ unknown
309
+ >;
177
310
  if ('cache_control' in block) {
178
311
  delete block.cache_control;
179
312
  }
180
313
  }
181
314
  }
182
315
 
183
- if (messagesModified >= 2 || isToolMessage) {
316
+ if (messagesModified >= 2 || isToolMessage || isEmptyString) {
184
317
  continue;
185
318
  }
186
319
 
187
- if (typeof content === 'string' && content === '') {
188
- continue;
189
- }
190
-
191
- if (typeof content === 'string') {
320
+ if (typeof message.content === 'string') {
192
321
  message.content = [
193
- { type: ContentTypes.TEXT, text: content },
322
+ { type: ContentTypes.TEXT, text: message.content },
194
323
  { cachePoint: { type: 'default' } },
195
324
  ] as MessageContentComplex[];
196
325
  messagesModified++;
@@ -0,0 +1,159 @@
1
+ // src/scripts/thinking-bedrock.ts
2
+ import { config } from 'dotenv';
3
+ config();
4
+ import { HumanMessage, BaseMessage } from '@langchain/core/messages';
5
+ import type { UsageMetadata } from '@langchain/core/messages';
6
+ import * as t from '@/types';
7
+ import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
8
+ import { createCodeExecutionTool } from '@/tools/CodeExecutor';
9
+ import { ToolEndHandler, ModelEndHandler } from '@/events';
10
+ import { GraphEvents, Providers } from '@/common';
11
+ import { getLLMConfig } from '@/utils/llmConfig';
12
+ import { getArgs } from '@/scripts/args';
13
+ import { Run } from '@/run';
14
+
15
+ const conversationHistory: BaseMessage[] = [];
16
+ let _contentParts: t.MessageContentComplex[] = [];
17
+ const collectedUsage: UsageMetadata[] = [];
18
+
19
+ async function testBedrockThinking(): Promise<void> {
20
+ const { userName } = await getArgs();
21
+ const instructions = `You are a helpful AI assistant for ${userName}. When answering questions, be thorough in your reasoning.`;
22
+ const { contentParts, aggregateContent } = createContentAggregator();
23
+ _contentParts = contentParts as t.MessageContentComplex[];
24
+
25
+ // Set up event handlers
26
+ const customHandlers = {
27
+ [GraphEvents.TOOL_END]: new ToolEndHandler(),
28
+ [GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage),
29
+ [GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
30
+ [GraphEvents.ON_RUN_STEP_COMPLETED]: {
31
+ handle: (
32
+ event: GraphEvents.ON_RUN_STEP_COMPLETED,
33
+ data: t.StreamEventData
34
+ ): void => {
35
+ console.log('====== ON_RUN_STEP_COMPLETED ======');
36
+ aggregateContent({
37
+ event,
38
+ data: data as unknown as { result: t.ToolEndEvent },
39
+ });
40
+ },
41
+ },
42
+ [GraphEvents.ON_RUN_STEP]: {
43
+ handle: (event: GraphEvents.ON_RUN_STEP, data: t.RunStep) => {
44
+ aggregateContent({ event, data });
45
+ },
46
+ },
47
+ [GraphEvents.ON_RUN_STEP_DELTA]: {
48
+ handle: (
49
+ event: GraphEvents.ON_RUN_STEP_DELTA,
50
+ data: t.RunStepDeltaEvent
51
+ ) => {
52
+ aggregateContent({ event, data });
53
+ },
54
+ },
55
+ [GraphEvents.ON_MESSAGE_DELTA]: {
56
+ handle: (
57
+ event: GraphEvents.ON_MESSAGE_DELTA,
58
+ data: t.MessageDeltaEvent
59
+ ) => {
60
+ aggregateContent({ event, data });
61
+ },
62
+ },
63
+ [GraphEvents.ON_REASONING_DELTA]: {
64
+ handle: (
65
+ event: GraphEvents.ON_REASONING_DELTA,
66
+ data: t.ReasoningDeltaEvent
67
+ ) => {
68
+ aggregateContent({ event, data });
69
+ },
70
+ },
71
+ };
72
+
73
+ const baseLlmConfig = getLLMConfig(Providers.BEDROCK);
74
+
75
+ // Enable thinking with token budget for Bedrock
76
+ const llmConfig = {
77
+ ...baseLlmConfig,
78
+ model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
79
+ maxTokens: 5000,
80
+ additionalModelRequestFields: {
81
+ thinking: { type: 'enabled', budget_tokens: 2000 },
82
+ },
83
+ };
84
+
85
+ const run = await Run.create<t.IState>({
86
+ runId: 'test-bedrock-thinking-id',
87
+ graphConfig: {
88
+ instructions,
89
+ type: 'standard',
90
+ tools: [createCodeExecutionTool()],
91
+ llmConfig,
92
+ },
93
+ returnContent: true,
94
+ customHandlers: customHandlers as t.RunConfig['customHandlers'],
95
+ });
96
+
97
+ const config = {
98
+ configurable: {
99
+ thread_id: 'bedrock-thinking-test-thread',
100
+ },
101
+ streamMode: 'values',
102
+ version: 'v2' as const,
103
+ };
104
+
105
+ // Test 1: Regular thinking mode
106
+ console.log('\n\nTest 1: Bedrock Regular thinking mode');
107
+ const userMessage1 = `Please print 'hello world' in python`;
108
+ conversationHistory.push(new HumanMessage(userMessage1));
109
+
110
+ console.log('Running first query with Bedrock thinking enabled...');
111
+ const firstInputs = { messages: [...conversationHistory] };
112
+ await run.processStream(firstInputs, config);
113
+
114
+ // Extract and display thinking blocks
115
+ const finalMessages = run.getRunMessages();
116
+ console.log('\n\nFinal messages after Test 1:');
117
+ console.dir(finalMessages, { depth: null });
118
+
119
+ // Test 2: Try multi-turn conversation
120
+ console.log(
121
+ '\n\nTest 2: Multi-turn conversation with Bedrock thinking enabled'
122
+ );
123
+ const userMessage2 = `Given your previous analysis, what would be the most significant technical challenges in making this transition?`;
124
+ conversationHistory.push(new HumanMessage(userMessage2));
125
+
126
+ console.log('Running second query with Bedrock thinking enabled...');
127
+ const secondInputs = { messages: [...conversationHistory] };
128
+ await run.processStream(secondInputs, config);
129
+
130
+ // Display thinking blocks for second response
131
+ const finalMessages2 = run.getRunMessages();
132
+ console.log('\n\nBedrock thinking feature test completed!');
133
+ console.dir(finalMessages2, { depth: null });
134
+
135
+ console.log('\n\nContent parts:');
136
+ console.dir(_contentParts, { depth: null });
137
+ }
138
+
139
+ process.on('unhandledRejection', (reason, promise) => {
140
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
141
+ console.log('Conversation history:');
142
+ console.dir(conversationHistory, { depth: null });
143
+ console.log('Content parts:');
144
+ console.dir(_contentParts, { depth: null });
145
+ process.exit(1);
146
+ });
147
+
148
+ process.on('uncaughtException', (err) => {
149
+ console.error('Uncaught Exception:', err);
150
+ });
151
+
152
+ testBedrockThinking().catch((err) => {
153
+ console.error(err);
154
+ console.log('Conversation history:');
155
+ console.dir(conversationHistory, { depth: null });
156
+ console.log('Content parts:');
157
+ console.dir(_contentParts, { depth: null });
158
+ process.exit(1);
159
+ });