@librechat/agents 3.1.34 → 3.1.35

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.
@@ -315,7 +315,7 @@ describe('CustomChatBedrockConverse', () => {
315
315
  expect(model.removeContentBlockIndex(undefined)).toBeUndefined();
316
316
  });
317
317
 
318
- test('cleanChunk should remove contentBlockIndex from AIMessageChunk response_metadata', () => {
318
+ test('enrichChunk should strip contentBlockIndex from response_metadata', () => {
319
319
  const model = getModelWithCleanMethods();
320
320
 
321
321
  const chunkWithIndex = new ChatGenerationChunk({
@@ -329,18 +329,18 @@ describe('CustomChatBedrockConverse', () => {
329
329
  }),
330
330
  });
331
331
 
332
- const cleaned = model.cleanChunk(chunkWithIndex);
332
+ const enriched = model.enrichChunk(chunkWithIndex, new Set([0]));
333
333
 
334
- expect(cleaned.message.response_metadata).toEqual({
334
+ expect(enriched.message.response_metadata).toEqual({
335
335
  stopReason: null,
336
336
  });
337
337
  expect(
338
- (cleaned.message.response_metadata as any).contentBlockIndex
338
+ (enriched.message.response_metadata as any).contentBlockIndex
339
339
  ).toBeUndefined();
340
- expect(cleaned.text).toBe('Hello');
340
+ expect(enriched.text).toBe('Hello');
341
341
  });
342
342
 
343
- test('cleanChunk should pass through chunks without contentBlockIndex unchanged', () => {
343
+ test('enrichChunk should pass through chunks without contentBlockIndex unchanged', () => {
344
344
  const model = getModelWithCleanMethods();
345
345
 
346
346
  const chunkWithoutIndex = new ChatGenerationChunk({
@@ -354,15 +354,89 @@ describe('CustomChatBedrockConverse', () => {
354
354
  }),
355
355
  });
356
356
 
357
- const cleaned = model.cleanChunk(chunkWithoutIndex);
357
+ const enriched = model.enrichChunk(chunkWithoutIndex, new Set());
358
358
 
359
- expect(cleaned.message.response_metadata).toEqual({
359
+ expect(enriched.message.response_metadata).toEqual({
360
360
  stopReason: 'end_turn',
361
361
  usage: { inputTokens: 10, outputTokens: 5 },
362
362
  });
363
363
  });
364
364
 
365
- test('cleanChunk should handle deeply nested contentBlockIndex in response_metadata', () => {
365
+ test('enrichChunk should inject index on array content blocks', () => {
366
+ const model = getModelWithCleanMethods();
367
+
368
+ const chunkWithArrayContent = new ChatGenerationChunk({
369
+ text: '',
370
+ message: new AIMessageChunk({
371
+ content: [
372
+ {
373
+ type: 'reasoning_content',
374
+ reasoningText: { text: 'thinking...' },
375
+ },
376
+ ],
377
+ response_metadata: {
378
+ contentBlockIndex: 0,
379
+ },
380
+ }),
381
+ });
382
+
383
+ const enriched = model.enrichChunk(chunkWithArrayContent, new Set([0]));
384
+
385
+ expect(Array.isArray(enriched.message.content)).toBe(true);
386
+ const blocks = enriched.message.content as any[];
387
+ expect(blocks[0].index).toBe(0);
388
+ expect(blocks[0].type).toBe('reasoning_content');
389
+ expect(
390
+ (enriched.message.response_metadata as any).contentBlockIndex
391
+ ).toBeUndefined();
392
+ });
393
+
394
+ test('enrichChunk should promote text to array when multiple block indices seen', () => {
395
+ const model = getModelWithCleanMethods();
396
+
397
+ const textChunk = new ChatGenerationChunk({
398
+ text: 'Hello world',
399
+ message: new AIMessageChunk({
400
+ content: 'Hello world',
401
+ response_metadata: {
402
+ contentBlockIndex: 1,
403
+ },
404
+ }),
405
+ });
406
+
407
+ const enriched = model.enrichChunk(textChunk, new Set([0, 1]));
408
+
409
+ expect(Array.isArray(enriched.message.content)).toBe(true);
410
+ const blocks = enriched.message.content as any[];
411
+ expect(blocks).toHaveLength(1);
412
+ expect(blocks[0]).toEqual({
413
+ type: 'text',
414
+ text: 'Hello world',
415
+ index: 1,
416
+ });
417
+ });
418
+
419
+ test('enrichChunk should keep text as string when only one block index seen', () => {
420
+ const model = getModelWithCleanMethods();
421
+
422
+ const textChunk = new ChatGenerationChunk({
423
+ text: 'Hello',
424
+ message: new AIMessageChunk({
425
+ content: 'Hello',
426
+ response_metadata: {
427
+ contentBlockIndex: 0,
428
+ stopReason: null,
429
+ },
430
+ }),
431
+ });
432
+
433
+ const enriched = model.enrichChunk(textChunk, new Set([0]));
434
+
435
+ expect(typeof enriched.message.content).toBe('string');
436
+ expect(enriched.message.content).toBe('Hello');
437
+ });
438
+
439
+ test('enrichChunk should strip deeply nested contentBlockIndex from response_metadata', () => {
366
440
  const model = getModelWithCleanMethods();
367
441
 
368
442
  const chunkWithNestedIndex = new ChatGenerationChunk({
@@ -370,6 +444,7 @@ describe('CustomChatBedrockConverse', () => {
370
444
  message: new AIMessageChunk({
371
445
  content: 'Test',
372
446
  response_metadata: {
447
+ contentBlockIndex: 0,
373
448
  amazon: {
374
449
  bedrock: {
375
450
  contentBlockIndex: 0,
@@ -381,9 +456,9 @@ describe('CustomChatBedrockConverse', () => {
381
456
  }),
382
457
  });
383
458
 
384
- const cleaned = model.cleanChunk(chunkWithNestedIndex);
459
+ const enriched = model.enrichChunk(chunkWithNestedIndex, new Set([0]));
385
460
 
386
- expect(cleaned.message.response_metadata).toEqual({
461
+ expect(enriched.message.response_metadata).toEqual({
387
462
  amazon: {
388
463
  bedrock: {
389
464
  trace: { something: 'value' },
@@ -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
+ });