@librechat/agents 3.1.34 → 3.1.36

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.
@@ -212,47 +212,6 @@ describe('CustomChatBedrockConverse', () => {
212
212
  return model as any;
213
213
  }
214
214
 
215
- test('should detect contentBlockIndex at top level', () => {
216
- const model = getModelWithCleanMethods();
217
- const objWithIndex = { contentBlockIndex: 0, text: 'hello' };
218
- const objWithoutIndex = { text: 'hello' };
219
-
220
- expect(model.hasContentBlockIndex(objWithIndex)).toBe(true);
221
- expect(model.hasContentBlockIndex(objWithoutIndex)).toBe(false);
222
- });
223
-
224
- test('should detect contentBlockIndex in nested objects', () => {
225
- const model = getModelWithCleanMethods();
226
- const nestedWithIndex = {
227
- outer: {
228
- inner: {
229
- contentBlockIndex: 1,
230
- data: 'test',
231
- },
232
- },
233
- };
234
- const nestedWithoutIndex = {
235
- outer: {
236
- inner: {
237
- data: 'test',
238
- },
239
- },
240
- };
241
-
242
- expect(model.hasContentBlockIndex(nestedWithIndex)).toBe(true);
243
- expect(model.hasContentBlockIndex(nestedWithoutIndex)).toBe(false);
244
- });
245
-
246
- test('should return false for null, undefined, and primitives', () => {
247
- const model = getModelWithCleanMethods();
248
-
249
- expect(model.hasContentBlockIndex(null)).toBe(false);
250
- expect(model.hasContentBlockIndex(undefined)).toBe(false);
251
- expect(model.hasContentBlockIndex('string')).toBe(false);
252
- expect(model.hasContentBlockIndex(123)).toBe(false);
253
- expect(model.hasContentBlockIndex(true)).toBe(false);
254
- });
255
-
256
215
  test('should remove contentBlockIndex from top level', () => {
257
216
  const model = getModelWithCleanMethods();
258
217
  const obj = {
@@ -315,7 +274,7 @@ describe('CustomChatBedrockConverse', () => {
315
274
  expect(model.removeContentBlockIndex(undefined)).toBeUndefined();
316
275
  });
317
276
 
318
- test('cleanChunk should remove contentBlockIndex from AIMessageChunk response_metadata', () => {
277
+ test('enrichChunk should strip contentBlockIndex from response_metadata', () => {
319
278
  const model = getModelWithCleanMethods();
320
279
 
321
280
  const chunkWithIndex = new ChatGenerationChunk({
@@ -329,18 +288,18 @@ describe('CustomChatBedrockConverse', () => {
329
288
  }),
330
289
  });
331
290
 
332
- const cleaned = model.cleanChunk(chunkWithIndex);
291
+ const enriched = model.enrichChunk(chunkWithIndex, new Set([0]));
333
292
 
334
- expect(cleaned.message.response_metadata).toEqual({
293
+ expect(enriched.message.response_metadata).toEqual({
335
294
  stopReason: null,
336
295
  });
337
296
  expect(
338
- (cleaned.message.response_metadata as any).contentBlockIndex
297
+ (enriched.message.response_metadata as any).contentBlockIndex
339
298
  ).toBeUndefined();
340
- expect(cleaned.text).toBe('Hello');
299
+ expect(enriched.text).toBe('Hello');
341
300
  });
342
301
 
343
- test('cleanChunk should pass through chunks without contentBlockIndex unchanged', () => {
302
+ test('enrichChunk should pass through chunks without contentBlockIndex unchanged', () => {
344
303
  const model = getModelWithCleanMethods();
345
304
 
346
305
  const chunkWithoutIndex = new ChatGenerationChunk({
@@ -354,15 +313,89 @@ describe('CustomChatBedrockConverse', () => {
354
313
  }),
355
314
  });
356
315
 
357
- const cleaned = model.cleanChunk(chunkWithoutIndex);
316
+ const enriched = model.enrichChunk(chunkWithoutIndex, new Set());
358
317
 
359
- expect(cleaned.message.response_metadata).toEqual({
318
+ expect(enriched.message.response_metadata).toEqual({
360
319
  stopReason: 'end_turn',
361
320
  usage: { inputTokens: 10, outputTokens: 5 },
362
321
  });
363
322
  });
364
323
 
365
- test('cleanChunk should handle deeply nested contentBlockIndex in response_metadata', () => {
324
+ test('enrichChunk should inject index on array content blocks', () => {
325
+ const model = getModelWithCleanMethods();
326
+
327
+ const chunkWithArrayContent = new ChatGenerationChunk({
328
+ text: '',
329
+ message: new AIMessageChunk({
330
+ content: [
331
+ {
332
+ type: 'reasoning_content',
333
+ reasoningText: { text: 'thinking...' },
334
+ },
335
+ ],
336
+ response_metadata: {
337
+ contentBlockIndex: 0,
338
+ },
339
+ }),
340
+ });
341
+
342
+ const enriched = model.enrichChunk(chunkWithArrayContent, new Set([0]));
343
+
344
+ expect(Array.isArray(enriched.message.content)).toBe(true);
345
+ const blocks = enriched.message.content as any[];
346
+ expect(blocks[0].index).toBe(0);
347
+ expect(blocks[0].type).toBe('reasoning_content');
348
+ expect(
349
+ (enriched.message.response_metadata as any).contentBlockIndex
350
+ ).toBeUndefined();
351
+ });
352
+
353
+ test('enrichChunk should promote text to array when multiple block indices seen', () => {
354
+ const model = getModelWithCleanMethods();
355
+
356
+ const textChunk = new ChatGenerationChunk({
357
+ text: 'Hello world',
358
+ message: new AIMessageChunk({
359
+ content: 'Hello world',
360
+ response_metadata: {
361
+ contentBlockIndex: 1,
362
+ },
363
+ }),
364
+ });
365
+
366
+ const enriched = model.enrichChunk(textChunk, new Set([0, 1]));
367
+
368
+ expect(Array.isArray(enriched.message.content)).toBe(true);
369
+ const blocks = enriched.message.content as any[];
370
+ expect(blocks).toHaveLength(1);
371
+ expect(blocks[0]).toEqual({
372
+ type: 'text',
373
+ text: 'Hello world',
374
+ index: 1,
375
+ });
376
+ });
377
+
378
+ test('enrichChunk should keep text as string when only one block index seen', () => {
379
+ const model = getModelWithCleanMethods();
380
+
381
+ const textChunk = new ChatGenerationChunk({
382
+ text: 'Hello',
383
+ message: new AIMessageChunk({
384
+ content: 'Hello',
385
+ response_metadata: {
386
+ contentBlockIndex: 0,
387
+ stopReason: null,
388
+ },
389
+ }),
390
+ });
391
+
392
+ const enriched = model.enrichChunk(textChunk, new Set([0]));
393
+
394
+ expect(typeof enriched.message.content).toBe('string');
395
+ expect(enriched.message.content).toBe('Hello');
396
+ });
397
+
398
+ test('enrichChunk should strip deeply nested contentBlockIndex from response_metadata', () => {
366
399
  const model = getModelWithCleanMethods();
367
400
 
368
401
  const chunkWithNestedIndex = new ChatGenerationChunk({
@@ -370,6 +403,7 @@ describe('CustomChatBedrockConverse', () => {
370
403
  message: new AIMessageChunk({
371
404
  content: 'Test',
372
405
  response_metadata: {
406
+ contentBlockIndex: 0,
373
407
  amazon: {
374
408
  bedrock: {
375
409
  contentBlockIndex: 0,
@@ -381,9 +415,9 @@ describe('CustomChatBedrockConverse', () => {
381
415
  }),
382
416
  });
383
417
 
384
- const cleaned = model.cleanChunk(chunkWithNestedIndex);
418
+ const enriched = model.enrichChunk(chunkWithNestedIndex, new Set([0]));
385
419
 
386
- expect(cleaned.message.response_metadata).toEqual({
420
+ expect(enriched.message.response_metadata).toEqual({
387
421
  amazon: {
388
422
  bedrock: {
389
423
  trace: { something: 'value' },
@@ -220,15 +220,12 @@ export function handleConverseStreamContentBlockDelta(
220
220
  bedrockReasoningDeltaToLangchainPartialReasoningBlock(
221
221
  contentBlockDelta.delta.reasoningContent
222
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
- : '';
223
+ let reasoningText = '';
224
+ if ('reasoningText' in reasoningBlock) {
225
+ reasoningText = reasoningBlock.reasoningText.text ?? '';
226
+ } else if ('redactedContent' in reasoningBlock) {
227
+ reasoningText = reasoningBlock.redactedContent;
228
+ }
232
229
  return new ChatGenerationChunk({
233
230
  text: '',
234
231
  message: new AIMessageChunk({
@@ -159,6 +159,11 @@ export function modifyDeltaProperties(
159
159
  (obj as Partial<AIMessageChunk>).lc_kwargs &&
160
160
  Array.isArray(obj.lc_kwargs.content)
161
161
  ) {
162
+ if (provider === Providers.BEDROCK) {
163
+ obj.lc_kwargs.content = reduceBlocks(
164
+ obj.lc_kwargs.content as ContentBlock[]
165
+ );
166
+ }
162
167
  obj.lc_kwargs.content = modifyContent({
163
168
  provider,
164
169
  messageType,
@@ -0,0 +1,107 @@
1
+ import { config } from 'dotenv';
2
+ config();
3
+ import { HumanMessage } from '@langchain/core/messages';
4
+ import type { AIMessageChunk } from '@langchain/core/messages';
5
+ import { concat } from '@langchain/core/utils/stream';
6
+ import { CustomChatBedrockConverse } from '@/llm/bedrock';
7
+ import { modifyDeltaProperties } from '@/messages/core';
8
+ import { Providers } from '@/common';
9
+
10
+ async function testBedrockMerge(): Promise<void> {
11
+ const model = new CustomChatBedrockConverse({
12
+ model: 'us.anthropic.claude-3-7-sonnet-20250219-v1:0',
13
+ region: process.env.BEDROCK_AWS_REGION,
14
+ credentials: {
15
+ accessKeyId: process.env.BEDROCK_AWS_ACCESS_KEY_ID!,
16
+ secretAccessKey: process.env.BEDROCK_AWS_SECRET_ACCESS_KEY!,
17
+ },
18
+ maxTokens: 4000,
19
+ streaming: true,
20
+ streamUsage: true,
21
+ additionalModelRequestFields: {
22
+ thinking: { type: 'enabled', budget_tokens: 2000 },
23
+ },
24
+ });
25
+
26
+ const messages = [new HumanMessage('What is 25 * 37? Think step by step.')];
27
+
28
+ console.log('Streaming from Bedrock with thinking enabled...\n');
29
+
30
+ const stream = await model.stream(messages);
31
+ let finalChunk: AIMessageChunk | undefined;
32
+ let chunkCount = 0;
33
+ let firstTextLogged = false;
34
+
35
+ for await (const chunk of stream) {
36
+ chunkCount++;
37
+ const isArr = Array.isArray(chunk.content);
38
+ const isStr = typeof chunk.content === 'string';
39
+ const isTextStr = isStr && (chunk.content as string).length > 0;
40
+
41
+ if (!firstTextLogged && isTextStr) {
42
+ console.log(
43
+ `chunk ${chunkCount} (first text): contentType=string, value="${chunk.content}"`
44
+ );
45
+ console.log(
46
+ ' response_metadata:',
47
+ JSON.stringify(chunk.response_metadata)
48
+ );
49
+ firstTextLogged = true;
50
+ }
51
+
52
+ if (isArr) {
53
+ const blocks = chunk.content as Array<Record<string, unknown>>;
54
+ const info = blocks.map((b) => ({
55
+ type: b.type,
56
+ hasIndex: 'index' in b,
57
+ index: b.index,
58
+ }));
59
+ console.log(`chunk ${chunkCount}: array content, blocks:`, info);
60
+ }
61
+
62
+ finalChunk = finalChunk ? concat(finalChunk, chunk) : chunk;
63
+ }
64
+
65
+ console.log(`Total chunks received: ${chunkCount}\n`);
66
+
67
+ console.log('=== RAW concat result (before modifyDeltaProperties) ===');
68
+ console.log('content type:', typeof finalChunk!.content);
69
+ if (Array.isArray(finalChunk!.content)) {
70
+ console.log('content array length:', finalChunk!.content.length);
71
+ const types = finalChunk!.content.map((b) =>
72
+ typeof b === 'object' && 'type' in b ? b.type : typeof b
73
+ );
74
+ const typeCounts = types.reduce(
75
+ (acc, t) => {
76
+ acc[t ?? ''] = (acc[t ?? ''] || 0) + 1;
77
+ return acc;
78
+ },
79
+ {} as Record<string, number>
80
+ );
81
+ console.log('content block type counts:', typeCounts);
82
+ }
83
+
84
+ console.log('\ncontent:');
85
+ console.dir(finalChunk!.content, { depth: null });
86
+
87
+ console.log('\n=== lc_kwargs.content ===');
88
+ if (Array.isArray(finalChunk!.lc_kwargs.content)) {
89
+ console.log(
90
+ 'lc_kwargs.content length:',
91
+ finalChunk!.lc_kwargs.content.length
92
+ );
93
+ }
94
+ console.dir(finalChunk!.lc_kwargs.content, { depth: null });
95
+
96
+ const modified = modifyDeltaProperties(Providers.BEDROCK, finalChunk);
97
+ console.log('\n=== After modifyDeltaProperties ===');
98
+ console.log('content:');
99
+ console.dir(modified!.content, { depth: null });
100
+ console.log('\nlc_kwargs.content:');
101
+ console.dir(modified!.lc_kwargs.content, { depth: null });
102
+ }
103
+
104
+ testBedrockMerge().catch((err) => {
105
+ console.error(err);
106
+ process.exit(1);
107
+ });