@librechat/agents 3.2.46 → 3.2.51

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.
Files changed (37) hide show
  1. package/dist/cjs/llm/anthropic/index.cjs +4 -3
  2. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  3. package/dist/cjs/llm/anthropic/utils/tools.cjs +2 -1
  4. package/dist/cjs/llm/anthropic/utils/tools.cjs.map +1 -1
  5. package/dist/cjs/llm/bedrock/cachePoints.cjs +28 -0
  6. package/dist/cjs/llm/bedrock/cachePoints.cjs.map +1 -0
  7. package/dist/cjs/llm/bedrock/index.cjs +10 -1
  8. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  9. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  10. package/dist/esm/llm/anthropic/index.mjs +4 -3
  11. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  12. package/dist/esm/llm/anthropic/utils/tools.mjs +2 -1
  13. package/dist/esm/llm/anthropic/utils/tools.mjs.map +1 -1
  14. package/dist/esm/llm/bedrock/cachePoints.mjs +28 -0
  15. package/dist/esm/llm/bedrock/cachePoints.mjs.map +1 -0
  16. package/dist/esm/llm/bedrock/index.mjs +10 -1
  17. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  18. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  19. package/dist/types/llm/anthropic/utils/tools.d.ts +1 -1
  20. package/dist/types/llm/bedrock/cachePoints.d.ts +9 -0
  21. package/package.json +16 -21
  22. package/src/llm/anthropic/index.ts +13 -2
  23. package/src/llm/anthropic/inherited-content-utils.spec.ts +244 -0
  24. package/src/llm/anthropic/inherited-stream-events.spec.ts +752 -0
  25. package/src/llm/anthropic/inherited-strict.spec.ts +302 -0
  26. package/src/llm/anthropic/llm.spec.ts +65 -0
  27. package/src/llm/anthropic/utils/tools.ts +7 -1
  28. package/src/llm/bedrock/cachePoints.ts +86 -0
  29. package/src/llm/bedrock/index.ts +9 -0
  30. package/src/llm/bedrock/inherited-cache.spec.ts +144 -0
  31. package/src/llm/bedrock/inherited.spec.ts +724 -0
  32. package/src/llm/google/inherited-stream-events.spec.ts +350 -0
  33. package/src/llm/openai/inherited-deepseek.spec.ts +347 -0
  34. package/src/llm/openai/inherited-xai.spec.ts +416 -0
  35. package/src/llm/openai/llm.spec.ts +1568 -0
  36. package/src/llm/openrouter/index.ts +1 -3
  37. package/src/llm/vertexai/inherited-stream-events.spec.ts +271 -0
@@ -0,0 +1,724 @@
1
+ // Inherited from @langchain/aws@1.4.2
2
+ // tests/chat_models_stream_events.test.ts (native streamEvents)
3
+ // tests/chat_models.invocation.test.ts (invocationParams / system / bearer / headers)
4
+ // tests/convertToConverseTools.test.ts (tool conversion util)
5
+ // utils/tests/message_outputs.test.ts (usage-metadata conversion util)
6
+ //
7
+ // Consolidates the deterministic, unit-testable cases from the four upstream
8
+ // suites and runs them against OUR fork, `CustomChatBedrockConverse` (imported
9
+ // here as `ChatBedrockConverse` from `@/llm/bedrock`).
10
+ //
11
+ // Applicability to our fork:
12
+ // `CustomChatBedrockConverse extends ChatBedrockConverse` (the installed
13
+ // @langchain/aws@1.4.2). It overrides `invocationParams`, `_streamResponseChunks`,
14
+ // `_generateNonStreaming`, and `getModelId`, but does NOT override the native
15
+ // `_streamChatModelEvents` (the streamEvents path) — that is inherited verbatim,
16
+ // so the streamEvents cases are a parity check that our overridden
17
+ // `_streamResponseChunks` still feeds the inherited event machinery correctly.
18
+ // Bearer-token auth and the defaultHeaders middleware are constructor-level
19
+ // features inherited unchanged from the base class.
20
+ //
21
+ // Adaptation (vitest -> jest):
22
+ // - `@jest/globals`; `vi.*` -> `jest.*`; `zod/v3` -> `zod` (not needed; see below).
23
+ // - Import the class from `@/llm/bedrock`, not `../chat_models.js`.
24
+ // - Upstream drove the SDK via `vi.mock('@aws-sdk/client-bedrock-runtime', ...)`.
25
+ // We instead use the harness from `llm.spec.ts`: inject a mock `client.send`
26
+ // (capturing the real `ConverseCommand`/`ConverseStreamCommand`), or spy on a
27
+ // real injected client's `middlewareStack.add`. This keeps the SDK command
28
+ // classes real and exercises the fork's real generation paths — no live API.
29
+ // - Upstream's streamEvents suite used vitest custom matchers
30
+ // (`toHaveStreamText` / `toHaveStreamReasoning` / `toHaveStreamToolCalls` /
31
+ // `toHaveStreamUsage`). Those don't exist in jest; they wrap the public
32
+ // `ChatModelStream` sub-streams (`.text` / `.reasoning` / `.toolCalls` /
33
+ // `.usage`), which we assert directly.
34
+ //
35
+ // Dropped (inherited): tests/convertToConverseTools.test.ts — the whole file.
36
+ // `convertToConverseTools` and `supportedToolChoiceValuesForModel` live in
37
+ // @langchain/aws's internal `utils/tools.js`, are NOT on the package's public
38
+ // export surface, and our fork neither uses nor re-exports them (verified via
39
+ // grep over src/ and the package `exports` map). Nothing in our fork to assert.
40
+ //
41
+ // Dropped (inherited): live — none of the four source files contained live
42
+ // `.invoke()`/`.stream()`-to-API cases; every retained case runs against a
43
+ // mocked transport.
44
+
45
+ /* eslint-disable @typescript-eslint/no-explicit-any */
46
+ import { test, expect, describe, jest, afterEach } from '@jest/globals';
47
+ import { ChatModelStream } from '@langchain/core/language_models/stream';
48
+ import {
49
+ HumanMessage,
50
+ SystemMessage,
51
+ AIMessageChunk,
52
+ } from '@langchain/core/messages';
53
+ import {
54
+ BedrockRuntimeClient,
55
+ ConverseCommand,
56
+ ConverseStreamCommand,
57
+ } from '@aws-sdk/client-bedrock-runtime';
58
+ import type {
59
+ Message as BedrockMessage,
60
+ ConverseResponse,
61
+ ConverseCommandInput,
62
+ ConverseStreamCommandInput,
63
+ } from '@aws-sdk/client-bedrock-runtime';
64
+ import type { BaseChatModelCallOptions } from '@langchain/core/language_models/chat_models';
65
+ import type { ChatModelStreamEvent } from '@langchain/core/language_models/event';
66
+ import {
67
+ convertConverseMessageToLangChainMessage,
68
+ handleConverseStreamMetadata,
69
+ } from './utils';
70
+ import { CustomChatBedrockConverse as ChatBedrockConverse } from './index';
71
+
72
+ jest.setTimeout(120000);
73
+
74
+ const baseConstructorArgs = {
75
+ region: 'us-east-1',
76
+ credentials: {
77
+ secretAccessKey: 'test-secret',
78
+ accessKeyId: 'test-key',
79
+ },
80
+ model: 'anthropic.claude-3-sonnet-20240229-v1:0',
81
+ };
82
+
83
+ afterEach(() => {
84
+ jest.restoreAllMocks();
85
+ });
86
+
87
+ // ─── Mock transport helpers ─────────────────────────────────────
88
+ //
89
+ // Mirrors `llm.spec.ts`: inject a mock `client.send` that captures the real
90
+ // command objects and returns canned non-stream / stream responses.
91
+
92
+ type CapturingClient = {
93
+ client: BedrockRuntimeClient;
94
+ sent: unknown[];
95
+ };
96
+
97
+ function nonStreamingResponse(): unknown {
98
+ return {
99
+ output: {
100
+ message: { role: 'assistant', content: [{ text: 'Response' }] },
101
+ },
102
+ stopReason: 'end_turn',
103
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
104
+ };
105
+ }
106
+
107
+ function streamingResponse(events: Array<Record<string, unknown>>): unknown {
108
+ return {
109
+ stream: (async function* () {
110
+ for (const event of events) {
111
+ yield event;
112
+ }
113
+ })(),
114
+ };
115
+ }
116
+
117
+ function capturingClient(makeResponse: () => unknown): CapturingClient {
118
+ const sent: unknown[] = [];
119
+ const send = jest
120
+ .fn<(command: unknown) => Promise<unknown>>()
121
+ .mockImplementation(async (command) => {
122
+ sent.push(command);
123
+ return makeResponse();
124
+ });
125
+ return { client: { send } as unknown as BedrockRuntimeClient, sent };
126
+ }
127
+
128
+ function streamEventsGen(
129
+ model: ChatBedrockConverse,
130
+ messages: HumanMessage[],
131
+ options: BaseChatModelCallOptions = {} as BaseChatModelCallOptions
132
+ ): AsyncGenerator<ChatModelStreamEvent> {
133
+ return (
134
+ model as unknown as {
135
+ _streamChatModelEvents: (
136
+ messages: unknown[],
137
+ options: BaseChatModelCallOptions
138
+ ) => AsyncGenerator<ChatModelStreamEvent>;
139
+ }
140
+ )._streamChatModelEvents(messages, options);
141
+ }
142
+
143
+ // ─── streamEvents fixtures (Bedrock Converse stream events) ─────
144
+
145
+ function bedrockTextStream(): Array<Record<string, unknown>> {
146
+ return [
147
+ { contentBlockDelta: { contentBlockIndex: 0, delta: { text: 'Hello' } } },
148
+ { contentBlockDelta: { contentBlockIndex: 0, delta: { text: ' world' } } },
149
+ ];
150
+ }
151
+
152
+ function bedrockReasoningStream(): Array<Record<string, unknown>> {
153
+ return [
154
+ {
155
+ contentBlockDelta: {
156
+ contentBlockIndex: 0,
157
+ delta: { reasoningContent: { text: 'Let me reason...' } },
158
+ },
159
+ },
160
+ ];
161
+ }
162
+
163
+ function bedrockToolStream(): Array<Record<string, unknown>> {
164
+ return [
165
+ {
166
+ contentBlockDelta: {
167
+ contentBlockIndex: 0,
168
+ delta: { text: 'Let me search.' },
169
+ },
170
+ },
171
+ {
172
+ contentBlockStart: {
173
+ contentBlockIndex: 1,
174
+ start: { toolUse: { toolUseId: 'toolu_1', name: 'web_search' } },
175
+ },
176
+ },
177
+ {
178
+ contentBlockDelta: {
179
+ contentBlockIndex: 1,
180
+ delta: { toolUse: { input: '{"query":"weather"}' } },
181
+ },
182
+ },
183
+ ];
184
+ }
185
+
186
+ function bedrockUsageStream(): Array<Record<string, unknown>> {
187
+ return [
188
+ {
189
+ metadata: {
190
+ usage: { inputTokens: 5, outputTokens: 2, totalTokens: 7 },
191
+ },
192
+ },
193
+ ];
194
+ }
195
+
196
+ function mockBedrock(
197
+ events: Array<Record<string, unknown>>
198
+ ): ChatBedrockConverse {
199
+ const { client } = capturingClient(() => streamingResponse(events));
200
+ return new ChatBedrockConverse({
201
+ ...baseConstructorArgs,
202
+ model: 'anthropic.claude-3-haiku-20240307-v1:0',
203
+ client,
204
+ });
205
+ }
206
+
207
+ // ─── streamEvents (native, inherited `_streamChatModelEvents`) ──
208
+ //
209
+ // From tests/chat_models_stream_events.test.ts. Upstream's custom matchers wrap
210
+ // the `ChatModelStream` sub-streams asserted here directly.
211
+ describe('CustomChatBedrockConverse.streamEvents (inherited native path)', () => {
212
+ test('streams text', async () => {
213
+ const stream = new ChatModelStream(
214
+ streamEventsGen(mockBedrock(bedrockTextStream()), [
215
+ new HumanMessage('Hello'),
216
+ ])
217
+ );
218
+ expect(await stream.text).toBe('Hello world');
219
+ });
220
+
221
+ test('streams reasoning', async () => {
222
+ const stream = new ChatModelStream(
223
+ streamEventsGen(mockBedrock(bedrockReasoningStream()), [
224
+ new HumanMessage('Hello'),
225
+ ])
226
+ );
227
+ expect(await stream.reasoning).toBe('Let me reason...');
228
+ });
229
+
230
+ test('streams tool calls', async () => {
231
+ const stream = new ChatModelStream(
232
+ streamEventsGen(mockBedrock(bedrockToolStream()), [
233
+ new HumanMessage('Hello'),
234
+ ])
235
+ );
236
+ const calls = await stream.toolCalls;
237
+ expect(calls).toEqual([
238
+ expect.objectContaining({
239
+ name: 'web_search',
240
+ args: { query: 'weather' },
241
+ }),
242
+ ]);
243
+ });
244
+
245
+ test('streams usage', async () => {
246
+ const stream = new ChatModelStream(
247
+ streamEventsGen(
248
+ mockBedrock(bedrockUsageStream()),
249
+ [new HumanMessage('Hello')],
250
+ { streamUsage: true } as BaseChatModelCallOptions
251
+ )
252
+ );
253
+ // Fork note: our metadata handler always emits an `input_token_details`
254
+ // object (here empty `{}`) where upstream's matcher only checked the three
255
+ // top-level token counts. Assert the counts upstream asserted via toMatchObject.
256
+ expect(await stream.usage).toMatchObject({
257
+ input_tokens: 5,
258
+ output_tokens: 2,
259
+ total_tokens: 7,
260
+ });
261
+ });
262
+ });
263
+
264
+ // ─── invocationParams / system / inferenceConfig ───────────────
265
+ //
266
+ // From tests/chat_models.invocation.test.ts. Upstream read the command input via
267
+ // `Reflect.get(ConverseCommand, 'lastInput')` from a vi.mock'd SDK; we read the
268
+ // real captured command's `.input` instead.
269
+ describe('CustomChatBedrockConverse invocationParams', () => {
270
+ describe('inferenceConfig conditional logic', () => {
271
+ test('covers all inferenceConfig scenarios compactly', () => {
272
+ const cases: Array<{
273
+ name: string;
274
+ ctor?: Partial<{
275
+ maxTokens: number;
276
+ temperature: number;
277
+ topP: number;
278
+ }>;
279
+ opts?: { stop?: string[] };
280
+ expectDefined: boolean;
281
+ expectValues?: Partial<{
282
+ maxTokens: number;
283
+ temperature: number;
284
+ topP: number;
285
+ stopSequences: string[];
286
+ }>;
287
+ expectUndefinedKeys?: Array<
288
+ 'maxTokens' | 'temperature' | 'topP' | 'stopSequences'
289
+ >;
290
+ }> = [
291
+ {
292
+ name: 'undefined when no inference values are set',
293
+ expectDefined: false,
294
+ },
295
+ {
296
+ name: 'includes only maxTokens when set',
297
+ ctor: { maxTokens: 100 },
298
+ expectDefined: true,
299
+ expectValues: { maxTokens: 100 },
300
+ expectUndefinedKeys: ['temperature', 'topP', 'stopSequences'],
301
+ },
302
+ {
303
+ name: 'includes only temperature when set',
304
+ ctor: { temperature: 0.7 },
305
+ expectDefined: true,
306
+ expectValues: { temperature: 0.7 },
307
+ expectUndefinedKeys: ['maxTokens', 'topP', 'stopSequences'],
308
+ },
309
+ {
310
+ name: 'includes only topP when set',
311
+ ctor: { topP: 0.9 },
312
+ expectDefined: true,
313
+ expectValues: { topP: 0.9 },
314
+ expectUndefinedKeys: ['maxTokens', 'temperature', 'stopSequences'],
315
+ },
316
+ {
317
+ name: 'includes stopSequences when provided',
318
+ opts: { stop: ['END', 'STOP'] },
319
+ expectDefined: true,
320
+ expectValues: { stopSequences: ['END', 'STOP'] },
321
+ expectUndefinedKeys: ['maxTokens', 'temperature', 'topP'],
322
+ },
323
+ {
324
+ name: 'includes all values when all are set',
325
+ ctor: { maxTokens: 200, temperature: 0.5, topP: 0.95 },
326
+ opts: { stop: ['END'] },
327
+ expectDefined: true,
328
+ expectValues: {
329
+ maxTokens: 200,
330
+ temperature: 0.5,
331
+ topP: 0.95,
332
+ stopSequences: ['END'],
333
+ },
334
+ },
335
+ {
336
+ name: 'undefined when stop sequences is empty array',
337
+ opts: { stop: [] },
338
+ expectDefined: false,
339
+ },
340
+ ];
341
+
342
+ for (const c of cases) {
343
+ const model = new ChatBedrockConverse({
344
+ ...baseConstructorArgs,
345
+ ...(c.ctor ?? {}),
346
+ });
347
+ const params = model.invocationParams(c.opts);
348
+ if (!c.expectDefined) {
349
+ expect(params.inferenceConfig).toBeUndefined();
350
+ continue;
351
+ }
352
+ expect(params.inferenceConfig).toBeDefined();
353
+ const ic = params.inferenceConfig as Record<string, unknown>;
354
+ if (c.expectValues?.maxTokens !== undefined) {
355
+ expect(ic.maxTokens).toBe(c.expectValues.maxTokens);
356
+ }
357
+ if (c.expectValues?.temperature !== undefined) {
358
+ expect(ic.temperature).toBe(c.expectValues.temperature);
359
+ }
360
+ if (c.expectValues?.topP !== undefined) {
361
+ expect(ic.topP).toBe(c.expectValues.topP);
362
+ }
363
+ if (c.expectValues?.stopSequences !== undefined) {
364
+ expect(ic.stopSequences).toEqual(c.expectValues.stopSequences);
365
+ }
366
+ for (const k of c.expectUndefinedKeys ?? []) {
367
+ expect(ic[k]).toBeUndefined();
368
+ }
369
+ }
370
+ });
371
+ });
372
+
373
+ describe('system parameter conditional logic (invoke)', () => {
374
+ test.each([
375
+ {
376
+ name: 'no system messages',
377
+ messages: [new HumanMessage('Hello')],
378
+ expectedSystem: { present: false, length: 0, texts: [] as string[] },
379
+ },
380
+ {
381
+ name: 'one system message',
382
+ messages: [
383
+ new SystemMessage('You are a helpful assistant.'),
384
+ new HumanMessage('Hello'),
385
+ ],
386
+ expectedSystem: {
387
+ present: true,
388
+ length: 1,
389
+ texts: ['You are a helpful assistant.'],
390
+ },
391
+ },
392
+ {
393
+ name: 'multiple system messages',
394
+ messages: [
395
+ new SystemMessage('You are a helpful assistant.'),
396
+ new SystemMessage('Be concise in your responses.'),
397
+ new HumanMessage('Hello'),
398
+ ],
399
+ expectedSystem: {
400
+ present: true,
401
+ length: 2,
402
+ texts: [
403
+ 'You are a helpful assistant.',
404
+ 'Be concise in your responses.',
405
+ ],
406
+ },
407
+ },
408
+ ])(
409
+ 'invoke should handle system parameter: $name',
410
+ async ({ messages, expectedSystem }) => {
411
+ const { client, sent } = capturingClient(nonStreamingResponse);
412
+ const model = new ChatBedrockConverse({
413
+ ...baseConstructorArgs,
414
+ client,
415
+ });
416
+ await model.invoke(messages);
417
+ const command = sent[0] as ConverseCommand;
418
+ expect(command).toBeInstanceOf(ConverseCommand);
419
+ const input = command.input as ConverseCommandInput;
420
+ if (!expectedSystem.present) {
421
+ expect(input).not.toHaveProperty('system');
422
+ return;
423
+ }
424
+ expect(input).toHaveProperty('system');
425
+ const system = input.system as NonNullable<typeof input.system>;
426
+ expect(system).toHaveLength(expectedSystem.length);
427
+ expectedSystem.texts.forEach((t, i) => {
428
+ expect(system[i]).toHaveProperty('text', t);
429
+ });
430
+ }
431
+ );
432
+ });
433
+
434
+ describe('stream method system parameter logic', () => {
435
+ test.each([
436
+ {
437
+ name: 'no system messages',
438
+ messages: [new HumanMessage('Hello')],
439
+ expectedPresent: false,
440
+ expectedLength: 0,
441
+ expectedTexts: [] as string[],
442
+ },
443
+ {
444
+ name: 'one system message',
445
+ messages: [
446
+ new SystemMessage('You are a helpful assistant.'),
447
+ new HumanMessage('Hello'),
448
+ ],
449
+ expectedPresent: true,
450
+ expectedLength: 1,
451
+ expectedTexts: ['You are a helpful assistant.'],
452
+ },
453
+ ])(
454
+ 'stream should handle system parameter: $name',
455
+ async ({ messages, expectedPresent, expectedLength, expectedTexts }) => {
456
+ const { client, sent } = capturingClient(() =>
457
+ streamingResponse([
458
+ {
459
+ contentBlockDelta: {
460
+ contentBlockIndex: 0,
461
+ delta: { text: 'Response' },
462
+ },
463
+ },
464
+ {
465
+ metadata: {
466
+ usage: { inputTokens: 10, outputTokens: 5, totalTokens: 15 },
467
+ },
468
+ },
469
+ ])
470
+ );
471
+ const model = new ChatBedrockConverse({
472
+ ...baseConstructorArgs,
473
+ client,
474
+ });
475
+ const stream = await model.stream(messages);
476
+ let chunks = 0;
477
+ for await (const _chunk of stream) {
478
+ chunks += 1;
479
+ }
480
+ expect(chunks).toBeGreaterThan(0);
481
+ const command = sent[0] as ConverseStreamCommand;
482
+ expect(command).toBeInstanceOf(ConverseStreamCommand);
483
+ const input = command.input as ConverseStreamCommandInput;
484
+ if (!expectedPresent) {
485
+ // Fork divergence: our `_streamResponseChunks` always spreads
486
+ // `system: converseSystem` into the command, so with no system message
487
+ // it sends an empty `system: []` rather than omitting it like upstream
488
+ // (and like our non-stream invoke path). Bedrock accepts an empty
489
+ // system array. Assert OURS.
490
+ expect(input.system).toEqual([]);
491
+ return;
492
+ }
493
+ expect(input).toHaveProperty('system');
494
+ const system = input.system as NonNullable<typeof input.system>;
495
+ expect(system).toHaveLength(expectedLength);
496
+ expectedTexts.forEach((t, i) => {
497
+ expect(system[i]).toHaveProperty('text', t);
498
+ });
499
+ }
500
+ );
501
+ });
502
+
503
+ // Dropped (inherited): "prompt caching request mapping" (invoke/stream map
504
+ // cache_control to system/messages/tools) — cache_control cases are owned by a
505
+ // separate agent (inherited-cache.spec.ts).
506
+
507
+ describe('bearer token auth (inherited from base constructor)', () => {
508
+ // Upstream asserted on a vi.mock'd `BedrockRuntimeClient.lastConfig`. We
509
+ // assert on the real constructed client's resolved config instead, which
510
+ // exercises the actual SDK auth wiring our fork inherits unchanged.
511
+ test('configures bearer auth from constructor token', async () => {
512
+ const model = new ChatBedrockConverse({
513
+ ...baseConstructorArgs,
514
+ bedrockBearerToken: 'test-bearer-token',
515
+ } as never);
516
+ expect(
517
+ (model as unknown as { bedrockBearerToken?: string }).bedrockBearerToken
518
+ ).toBe('test-bearer-token');
519
+
520
+ const config = (
521
+ model as unknown as {
522
+ client: {
523
+ config: {
524
+ authSchemePreference?: (() => Promise<string[]>) | string[];
525
+ credentials?: unknown;
526
+ token?: () => Promise<{ token: string }>;
527
+ };
528
+ };
529
+ }
530
+ ).client.config;
531
+
532
+ const authPref = config.authSchemePreference;
533
+ const resolvedAuth =
534
+ typeof authPref === 'function' ? await authPref() : authPref;
535
+ expect(resolvedAuth).toEqual(['httpBearerAuth']);
536
+ await expect(config.token?.()).resolves.toEqual({
537
+ token: 'test-bearer-token',
538
+ });
539
+ });
540
+
541
+ test('configures bearer auth from AWS_BEARER_TOKEN_BEDROCK', async () => {
542
+ process.env.AWS_BEARER_TOKEN_BEDROCK = 'env-bearer-token';
543
+ try {
544
+ const model = new ChatBedrockConverse({
545
+ ...baseConstructorArgs,
546
+ } as never);
547
+ expect(
548
+ (model as unknown as { bedrockBearerToken?: string })
549
+ .bedrockBearerToken
550
+ ).toBe('env-bearer-token');
551
+
552
+ const config = (
553
+ model as unknown as {
554
+ client: {
555
+ config: {
556
+ authSchemePreference?: (() => Promise<string[]>) | string[];
557
+ token?: () => Promise<{ token: string }>;
558
+ };
559
+ };
560
+ }
561
+ ).client.config;
562
+ const authPref = config.authSchemePreference;
563
+ const resolvedAuth =
564
+ typeof authPref === 'function' ? await authPref() : authPref;
565
+ expect(resolvedAuth).toEqual(['httpBearerAuth']);
566
+ await expect(config.token?.()).resolves.toEqual({
567
+ token: 'env-bearer-token',
568
+ });
569
+ } finally {
570
+ delete process.env.AWS_BEARER_TOKEN_BEDROCK;
571
+ }
572
+ });
573
+ });
574
+
575
+ describe('defaultHeaders middleware (inherited from base constructor)', () => {
576
+ // Spy on a real injected client's `middlewareStack.add` (instead of a
577
+ // vi.mock'd stack) so the actual header-injection middleware is captured and
578
+ // invoked.
579
+ test('registers middleware on client when defaultHeaders are provided', () => {
580
+ const injected = new BedrockRuntimeClient({
581
+ region: 'us-east-1',
582
+ credentials: { secretAccessKey: 's', accessKeyId: 'a' },
583
+ });
584
+ const addSpy = jest.spyOn(injected.middlewareStack, 'add');
585
+
586
+ new ChatBedrockConverse({
587
+ ...baseConstructorArgs,
588
+ defaultHeaders: {
589
+ 'X-Foo': 'Bar',
590
+ 'anthropic-beta': 'prompt-caching-2024-07-31',
591
+ },
592
+ client: injected,
593
+ } as never);
594
+
595
+ expect(addSpy).toHaveBeenCalledTimes(1);
596
+ const [middlewareFn, options] = addSpy.mock.calls[0] as [
597
+ (
598
+ next: (a: unknown) => Promise<unknown>
599
+ ) => (a: {
600
+ request: { headers: Record<string, string> };
601
+ }) => Promise<unknown>,
602
+ unknown,
603
+ ];
604
+ expect(options).toEqual({
605
+ step: 'build',
606
+ name: 'langchain_aws_default_headers',
607
+ });
608
+
609
+ const fakeRequest = { headers: {} as Record<string, string> };
610
+ const fakeNext = jest.fn<() => Promise<unknown>>().mockResolvedValue({});
611
+ void middlewareFn(fakeNext)({ request: fakeRequest });
612
+ expect(fakeRequest.headers['X-Foo']).toBe('Bar');
613
+ expect(fakeRequest.headers['anthropic-beta']).toBe(
614
+ 'prompt-caching-2024-07-31'
615
+ );
616
+ });
617
+
618
+ test('does not register middleware when defaultHeaders is absent', () => {
619
+ const injected = new BedrockRuntimeClient({
620
+ region: 'us-east-1',
621
+ credentials: { secretAccessKey: 's', accessKeyId: 'a' },
622
+ });
623
+ const addSpy = jest.spyOn(injected.middlewareStack, 'add');
624
+
625
+ new ChatBedrockConverse({
626
+ ...baseConstructorArgs,
627
+ client: injected,
628
+ } as never);
629
+
630
+ expect(addSpy).not.toHaveBeenCalled();
631
+ });
632
+ });
633
+ });
634
+
635
+ // ─── usage-metadata conversion utils ───────────────────────────
636
+ //
637
+ // From utils/tests/message_outputs.test.ts. Our fork re-exports these from
638
+ // `./utils` (utils/message_outputs.ts), so they are tested directly.
639
+ describe('message output usage metadata conversion', () => {
640
+ test('maps Bedrock prompt cache tokens for non-stream responses', () => {
641
+ const message: BedrockMessage = {
642
+ role: 'assistant',
643
+ content: [{ text: 'Hello' }],
644
+ };
645
+ const responseMetadata = {
646
+ usage: {
647
+ inputTokens: 10,
648
+ outputTokens: 5,
649
+ totalTokens: 25,
650
+ cacheReadInputTokens: 7,
651
+ cacheWriteInputTokens: 3,
652
+ },
653
+ } as Omit<ConverseResponse, 'output'>;
654
+
655
+ const result = convertConverseMessageToLangChainMessage(
656
+ message,
657
+ responseMetadata
658
+ );
659
+
660
+ // Fork divergence: upstream@1.4.2 folds cache read+write INTO input_tokens
661
+ // (would be 20). Our fork keeps input_tokens = raw inputTokens (10) and
662
+ // surfaces cache tokens only in input_token_details (Bedrock cache is
663
+ // additive, not a subset of input_tokens). Assert OURS.
664
+ expect(result.usage_metadata).toEqual({
665
+ input_tokens: 10,
666
+ output_tokens: 5,
667
+ total_tokens: 25,
668
+ input_token_details: {
669
+ cache_read: 7,
670
+ cache_creation: 3,
671
+ },
672
+ });
673
+ });
674
+
675
+ test('does not add input_token_details when Bedrock cache fields are absent', () => {
676
+ const message: BedrockMessage = {
677
+ role: 'assistant',
678
+ content: [{ text: 'Hello' }],
679
+ };
680
+ const responseMetadata = {
681
+ usage: {
682
+ inputTokens: 10,
683
+ outputTokens: 5,
684
+ totalTokens: 15,
685
+ },
686
+ } as Omit<ConverseResponse, 'output'>;
687
+
688
+ const result = convertConverseMessageToLangChainMessage(
689
+ message,
690
+ responseMetadata
691
+ );
692
+
693
+ expect(result.usage_metadata?.input_token_details).toBeUndefined();
694
+ });
695
+
696
+ test('maps Bedrock prompt cache tokens for stream metadata', () => {
697
+ const chunk = handleConverseStreamMetadata(
698
+ {
699
+ usage: {
700
+ inputTokens: 20,
701
+ outputTokens: 4,
702
+ totalTokens: 39,
703
+ cacheReadInputTokens: 9,
704
+ cacheWriteInputTokens: 6,
705
+ },
706
+ metrics: { latencyMs: 100 },
707
+ },
708
+ { streamUsage: true }
709
+ );
710
+ const message = chunk.message as AIMessageChunk;
711
+
712
+ // Same fork divergence as the non-stream case: upstream would report
713
+ // input_tokens 35 (20+9+6); our fork keeps the raw 20.
714
+ expect(message.usage_metadata).toEqual({
715
+ input_tokens: 20,
716
+ output_tokens: 4,
717
+ total_tokens: 39,
718
+ input_token_details: {
719
+ cache_read: 9,
720
+ cache_creation: 6,
721
+ },
722
+ });
723
+ });
724
+ });