@ai-sdk/amazon-bedrock 4.0.27 → 4.0.29

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 (39) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/anthropic/index.js +1 -1
  3. package/dist/anthropic/index.mjs +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/index.mjs +1 -1
  6. package/package.json +9 -5
  7. package/src/__fixtures__/bedrock-json-only-text-first.1.chunks.txt +0 -7
  8. package/src/__fixtures__/bedrock-json-other-tool.1.chunks.txt +0 -6
  9. package/src/__fixtures__/bedrock-json-other-tool.1.json +0 -24
  10. package/src/__fixtures__/bedrock-json-tool-text-then-weather-then-json.1.chunks.txt +0 -12
  11. package/src/__fixtures__/bedrock-json-tool-with-answer.1.json +0 -29
  12. package/src/__fixtures__/bedrock-json-tool.1.chunks.txt +0 -4
  13. package/src/__fixtures__/bedrock-json-tool.1.json +0 -35
  14. package/src/__fixtures__/bedrock-json-tool.2.chunks.txt +0 -6
  15. package/src/__fixtures__/bedrock-json-tool.2.json +0 -28
  16. package/src/__fixtures__/bedrock-json-tool.3.chunks.txt +0 -7
  17. package/src/__fixtures__/bedrock-json-tool.3.json +0 -36
  18. package/src/__fixtures__/bedrock-json-with-tool.1.chunks.txt +0 -9
  19. package/src/__fixtures__/bedrock-json-with-tool.1.json +0 -41
  20. package/src/__fixtures__/bedrock-json-with-tools.1.chunks.txt +0 -12
  21. package/src/__fixtures__/bedrock-json-with-tools.1.json +0 -50
  22. package/src/__fixtures__/bedrock-tool-call.1.chunks.txt +0 -6
  23. package/src/__fixtures__/bedrock-tool-call.1.json +0 -24
  24. package/src/__fixtures__/bedrock-tool-no-args.chunks.txt +0 -8
  25. package/src/__fixtures__/bedrock-tool-no-args.json +0 -25
  26. package/src/anthropic/bedrock-anthropic-fetch.test.ts +0 -344
  27. package/src/anthropic/bedrock-anthropic-provider.test.ts +0 -456
  28. package/src/bedrock-chat-language-model.test.ts +0 -4569
  29. package/src/bedrock-embedding-model.test.ts +0 -148
  30. package/src/bedrock-event-stream-response-handler.test.ts +0 -233
  31. package/src/bedrock-image-model.test.ts +0 -866
  32. package/src/bedrock-provider.test.ts +0 -457
  33. package/src/bedrock-sigv4-fetch.test.ts +0 -675
  34. package/src/convert-bedrock-usage.test.ts +0 -207
  35. package/src/convert-to-bedrock-chat-messages.test.ts +0 -1175
  36. package/src/inject-fetch-headers.test.ts +0 -135
  37. package/src/normalize-tool-call-id.test.ts +0 -72
  38. package/src/reranking/__fixtures__/bedrock-reranking.1.json +0 -12
  39. package/src/reranking/bedrock-reranking-model.test.ts +0 -299
@@ -1,4569 +0,0 @@
1
- import { LanguageModelV3Prompt } from '@ai-sdk/provider';
2
- import { createTestServer } from '@ai-sdk/test-server/with-vitest';
3
- import { convertReadableStreamToArray } from '@ai-sdk/provider-utils/test';
4
- import { BedrockChatLanguageModel } from './bedrock-chat-language-model';
5
- import { beforeEach, describe, expect, vi, it } from 'vitest';
6
- import { injectFetchHeaders } from './inject-fetch-headers';
7
- import {
8
- BedrockReasoningContentBlock,
9
- BedrockRedactedReasoningContentBlock,
10
- } from './bedrock-api-types';
11
- import { anthropicTools, prepareTools } from '@ai-sdk/anthropic/internal';
12
- import { z } from 'zod/v4';
13
- import fs from 'node:fs';
14
-
15
- const mockPrepareAnthropicTools = vi.mocked(prepareTools);
16
-
17
- vi.mock('@ai-sdk/anthropic/internal', async importOriginal => {
18
- const original =
19
- await importOriginal<typeof import('@ai-sdk/anthropic/internal')>();
20
- return {
21
- ...original,
22
- prepareTools: vi.fn(),
23
- anthropicTools: {
24
- ...original.anthropicTools,
25
- bash_20241022: (args: any) => ({
26
- type: 'provider',
27
- id: 'anthropic.bash_20241022',
28
- name: 'bash',
29
- args,
30
- inputSchema: z.object({
31
- command: z.string(),
32
- restart: z.boolean().optional(),
33
- }),
34
- }),
35
- },
36
- };
37
- });
38
-
39
- const TEST_PROMPT: LanguageModelV3Prompt = [
40
- { role: 'system', content: 'System Prompt' },
41
- { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
42
- ];
43
-
44
- const mockTrace = {
45
- guardrail: {
46
- inputAssessment: {
47
- '1abcd2ef34gh': {
48
- contentPolicy: {
49
- filters: [
50
- {
51
- action: 'BLOCKED' as const,
52
- confidence: 'LOW' as const,
53
- type: 'INSULTS' as const,
54
- },
55
- ],
56
- },
57
- wordPolicy: {
58
- managedWordLists: [
59
- {
60
- action: 'BLOCKED' as const,
61
- match: '<rude word>',
62
- type: 'PROFANITY' as const,
63
- },
64
- ],
65
- },
66
- },
67
- },
68
- },
69
- };
70
-
71
- const fakeFetchWithAuth = injectFetchHeaders({ 'x-amz-auth': 'test-auth' });
72
-
73
- const modelId = 'anthropic.claude-3-haiku-20240307-v1:0';
74
- const anthropicModelId = 'anthropic.claude-3-5-sonnet-20240620-v1:0'; // Define at top level
75
- const baseUrl = 'https://bedrock-runtime.us-east-1.amazonaws.com';
76
-
77
- const streamUrl = `${baseUrl}/model/${encodeURIComponent(
78
- modelId,
79
- )}/converse-stream`;
80
- const generateUrl = `${baseUrl}/model/${encodeURIComponent(modelId)}/converse`;
81
- const anthropicGenerateUrl = `${baseUrl}/model/${encodeURIComponent(
82
- anthropicModelId,
83
- )}/converse`;
84
-
85
- const novaModelId = 'us.amazon.nova-2-lite-v1:0';
86
- const novaGenerateUrl = `${baseUrl}/model/${encodeURIComponent(
87
- novaModelId,
88
- )}/converse`;
89
-
90
- const openaiModelId = 'openai.gpt-oss-120b-1:0';
91
- const openaiGenerateUrl = `${baseUrl}/model/${encodeURIComponent(
92
- openaiModelId,
93
- )}/converse`;
94
-
95
- const server = createTestServer({
96
- [generateUrl]: {},
97
- [streamUrl]: {
98
- response: {
99
- type: 'stream-chunks',
100
- chunks: [],
101
- },
102
- },
103
- // Configure the server for the Anthropic model from the start
104
- [anthropicGenerateUrl]: {},
105
- [novaGenerateUrl]: {},
106
- [openaiGenerateUrl]: {},
107
- });
108
-
109
- function prepareJsonFixtureResponse(filename: string) {
110
- server.urls[generateUrl].response = {
111
- type: 'json-value',
112
- body: JSON.parse(
113
- fs.readFileSync(`src/__fixtures__/${filename}.json`, 'utf8'),
114
- ),
115
- };
116
- return;
117
- }
118
-
119
- function prepareChunksFixtureResponse(filename: string) {
120
- const chunks = fs
121
- .readFileSync(`src/__fixtures__/${filename}.chunks.txt`, 'utf8')
122
- .split('\n')
123
- .filter(Boolean)
124
- .map(line => line + '\n');
125
-
126
- server.urls[streamUrl].response = {
127
- type: 'stream-chunks',
128
- chunks,
129
- };
130
- }
131
-
132
- beforeEach(() => {
133
- // Reset stream chunks for the default model
134
- server.urls[streamUrl].response = {
135
- type: 'stream-chunks',
136
- chunks: [],
137
- };
138
- // Reset the response for the anthropic model to a default empty state
139
- server.urls[anthropicGenerateUrl].response = {
140
- type: 'json-value',
141
- body: {},
142
- };
143
- mockPrepareAnthropicTools.mockClear();
144
- });
145
-
146
- const model = new BedrockChatLanguageModel(modelId, {
147
- baseUrl: () => baseUrl,
148
- headers: {},
149
- fetch: fakeFetchWithAuth,
150
- generateId: () => 'test-id',
151
- });
152
-
153
- const novaModel = new BedrockChatLanguageModel(novaModelId, {
154
- baseUrl: () => baseUrl,
155
- headers: {},
156
- fetch: fakeFetchWithAuth,
157
- generateId: () => 'test-id',
158
- });
159
-
160
- const openaiModel = new BedrockChatLanguageModel(openaiModelId, {
161
- baseUrl: () => baseUrl,
162
- headers: {},
163
- fetch: fakeFetchWithAuth,
164
- generateId: () => 'test-id',
165
- });
166
-
167
- let mockOptions: { success: boolean; errorValue?: any } = { success: true };
168
-
169
- describe('doStream', () => {
170
- beforeEach(() => {
171
- mockOptions = { success: true, errorValue: undefined };
172
- });
173
-
174
- vi.mock('./bedrock-event-stream-response-handler', () => ({
175
- createBedrockEventStreamResponseHandler: (schema: any) => {
176
- return async ({ response }: { response: Response }) => {
177
- let chunks: { success: boolean; value: any }[] = [];
178
- if (mockOptions.success) {
179
- const text = await response.text();
180
- chunks = text
181
- .split('\n')
182
- .filter(Boolean)
183
- .map(chunk => {
184
- const parsedChunk = JSON.parse(chunk);
185
- return {
186
- success: true,
187
- value: parsedChunk,
188
- rawValue: parsedChunk,
189
- };
190
- });
191
- }
192
- const headers = Object.fromEntries<string>([...response.headers]);
193
-
194
- return {
195
- responseHeaders: headers,
196
- value: new ReadableStream({
197
- start(controller) {
198
- if (mockOptions.success) {
199
- chunks.forEach(chunk => controller.enqueue(chunk));
200
- } else {
201
- controller.enqueue({
202
- success: false,
203
- error: mockOptions.errorValue,
204
- });
205
- }
206
- controller.close();
207
- },
208
- }),
209
- };
210
- };
211
- },
212
- }));
213
-
214
- function setupMockEventStreamHandler(
215
- options: { success?: boolean; errorValue?: any } = { success: true },
216
- ) {
217
- mockOptions = { ...mockOptions, ...options };
218
- }
219
-
220
- it('should stream text deltas with metadata and usage', async () => {
221
- setupMockEventStreamHandler();
222
- server.urls[streamUrl].response = {
223
- type: 'stream-chunks',
224
- chunks: [
225
- JSON.stringify({
226
- contentBlockDelta: {
227
- contentBlockIndex: 0,
228
- delta: { text: 'Hello' },
229
- },
230
- }) + '\n',
231
- JSON.stringify({
232
- contentBlockDelta: {
233
- contentBlockIndex: 1,
234
- delta: { text: ', ' },
235
- },
236
- }) + '\n',
237
- JSON.stringify({
238
- contentBlockDelta: {
239
- contentBlockIndex: 2,
240
- delta: { text: 'World!' },
241
- },
242
- }) + '\n',
243
- JSON.stringify({
244
- metadata: {
245
- usage: { inputTokens: 4, outputTokens: 34, totalTokens: 38 },
246
- metrics: { latencyMs: 10 },
247
- },
248
- }) + '\n',
249
- JSON.stringify({
250
- messageStop: {
251
- stopReason: 'stop_sequence',
252
- },
253
- }) + '\n',
254
- ],
255
- };
256
-
257
- const { stream } = await model.doStream({
258
- prompt: TEST_PROMPT,
259
- includeRawChunks: false,
260
- });
261
-
262
- expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
263
- [
264
- {
265
- "type": "stream-start",
266
- "warnings": [],
267
- },
268
- {
269
- "id": "0",
270
- "type": "text-start",
271
- },
272
- {
273
- "delta": "Hello",
274
- "id": "0",
275
- "type": "text-delta",
276
- },
277
- {
278
- "id": "1",
279
- "type": "text-start",
280
- },
281
- {
282
- "delta": ", ",
283
- "id": "1",
284
- "type": "text-delta",
285
- },
286
- {
287
- "id": "2",
288
- "type": "text-start",
289
- },
290
- {
291
- "delta": "World!",
292
- "id": "2",
293
- "type": "text-delta",
294
- },
295
- {
296
- "finishReason": {
297
- "raw": "stop_sequence",
298
- "unified": "stop",
299
- },
300
- "type": "finish",
301
- "usage": {
302
- "inputTokens": {
303
- "cacheRead": 0,
304
- "cacheWrite": 0,
305
- "noCache": 4,
306
- "total": 4,
307
- },
308
- "outputTokens": {
309
- "reasoning": undefined,
310
- "text": 34,
311
- "total": 34,
312
- },
313
- "raw": {
314
- "inputTokens": 4,
315
- "outputTokens": 34,
316
- "totalTokens": 38,
317
- },
318
- },
319
- },
320
- ]
321
- `);
322
- });
323
-
324
- it('should stream tool deltas', async () => {
325
- setupMockEventStreamHandler();
326
- server.urls[streamUrl].response = {
327
- type: 'stream-chunks',
328
- chunks: [
329
- JSON.stringify({
330
- contentBlockStart: {
331
- contentBlockIndex: 0,
332
- start: {
333
- toolUse: { toolUseId: 'tool-use-id', name: 'test-tool' },
334
- },
335
- },
336
- }) + '\n',
337
- JSON.stringify({
338
- contentBlockDelta: {
339
- contentBlockIndex: 0,
340
- delta: { toolUse: { input: '{"value":' } },
341
- },
342
- }) + '\n',
343
- JSON.stringify({
344
- contentBlockDelta: {
345
- contentBlockIndex: 0,
346
- delta: { toolUse: { input: '"Sparkle Day"}' } },
347
- },
348
- }) + '\n',
349
- JSON.stringify({
350
- contentBlockStop: { contentBlockIndex: 0 },
351
- }) + '\n',
352
- JSON.stringify({
353
- messageStop: {
354
- stopReason: 'tool_use',
355
- },
356
- }) + '\n',
357
- ],
358
- };
359
-
360
- const { stream } = await model.doStream({
361
- tools: [
362
- {
363
- type: 'function',
364
- name: 'test-tool',
365
- inputSchema: {
366
- type: 'object',
367
- properties: { value: { type: 'string' } },
368
- required: ['value'],
369
- additionalProperties: false,
370
- $schema: 'http://json-schema.org/draft-07/schema#',
371
- },
372
- },
373
- ],
374
- toolChoice: { type: 'tool', toolName: 'test-tool' },
375
- prompt: TEST_PROMPT,
376
- includeRawChunks: false,
377
- });
378
-
379
- expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
380
- [
381
- {
382
- "type": "stream-start",
383
- "warnings": [],
384
- },
385
- {
386
- "id": "tool-use-id",
387
- "toolName": "test-tool",
388
- "type": "tool-input-start",
389
- },
390
- {
391
- "delta": "{"value":",
392
- "id": "tool-use-id",
393
- "type": "tool-input-delta",
394
- },
395
- {
396
- "delta": ""Sparkle Day"}",
397
- "id": "tool-use-id",
398
- "type": "tool-input-delta",
399
- },
400
- {
401
- "id": "tool-use-id",
402
- "type": "tool-input-end",
403
- },
404
- {
405
- "input": "{"value":"Sparkle Day"}",
406
- "toolCallId": "tool-use-id",
407
- "toolName": "test-tool",
408
- "type": "tool-call",
409
- },
410
- {
411
- "finishReason": {
412
- "raw": "tool_use",
413
- "unified": "tool-calls",
414
- },
415
- "type": "finish",
416
- "usage": {
417
- "inputTokens": {
418
- "cacheRead": undefined,
419
- "cacheWrite": undefined,
420
- "noCache": undefined,
421
- "total": undefined,
422
- },
423
- "outputTokens": {
424
- "reasoning": undefined,
425
- "text": undefined,
426
- "total": undefined,
427
- },
428
- "raw": undefined,
429
- },
430
- },
431
- ]
432
- `);
433
- });
434
-
435
- it('should stream parallel tool calls', async () => {
436
- setupMockEventStreamHandler();
437
- server.urls[streamUrl].response = {
438
- type: 'stream-chunks',
439
- chunks: [
440
- JSON.stringify({
441
- contentBlockStart: {
442
- contentBlockIndex: 0,
443
- start: {
444
- toolUse: { toolUseId: 'tool-use-id-1', name: 'test-tool-1' },
445
- },
446
- },
447
- }) + '\n',
448
- JSON.stringify({
449
- contentBlockDelta: {
450
- contentBlockIndex: 0,
451
- delta: { toolUse: { input: '{"value1":' } },
452
- },
453
- }) + '\n',
454
- JSON.stringify({
455
- contentBlockStart: {
456
- contentBlockIndex: 1,
457
- start: {
458
- toolUse: { toolUseId: 'tool-use-id-2', name: 'test-tool-2' },
459
- },
460
- },
461
- }) + '\n',
462
- JSON.stringify({
463
- contentBlockDelta: {
464
- contentBlockIndex: 1,
465
- delta: { toolUse: { input: '{"value2":' } },
466
- },
467
- }) + '\n',
468
- JSON.stringify({
469
- contentBlockDelta: {
470
- contentBlockIndex: 1,
471
- delta: { toolUse: { input: '"Sparkle Day"}' } },
472
- },
473
- }) + '\n',
474
- JSON.stringify({
475
- contentBlockDelta: {
476
- contentBlockIndex: 0,
477
- delta: { toolUse: { input: '"Sparkle Day"}' } },
478
- },
479
- }) + '\n',
480
- JSON.stringify({
481
- contentBlockStop: { contentBlockIndex: 0 },
482
- }) + '\n',
483
- JSON.stringify({
484
- contentBlockStop: { contentBlockIndex: 1 },
485
- }) + '\n',
486
- JSON.stringify({
487
- messageStop: {
488
- stopReason: 'tool_use',
489
- },
490
- }) + '\n',
491
- ],
492
- };
493
-
494
- const { stream } = await model.doStream({
495
- tools: [
496
- {
497
- type: 'function',
498
- name: 'test-tool-1',
499
- inputSchema: {
500
- type: 'object',
501
- properties: { value1: { type: 'string' } },
502
- required: ['value'],
503
- additionalProperties: false,
504
- $schema: 'http://json-schema.org/draft-07/schema#',
505
- },
506
- },
507
- {
508
- type: 'function',
509
- name: 'test-tool-2',
510
- inputSchema: {
511
- type: 'object',
512
- properties: { value2: { type: 'string' } },
513
- required: ['value'],
514
- additionalProperties: false,
515
- $schema: 'http://json-schema.org/draft-07/schema#',
516
- },
517
- },
518
- ],
519
- toolChoice: { type: 'tool', toolName: 'test-tool' },
520
- prompt: TEST_PROMPT,
521
- includeRawChunks: false,
522
- });
523
-
524
- expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
525
- [
526
- {
527
- "type": "stream-start",
528
- "warnings": [],
529
- },
530
- {
531
- "id": "tool-use-id-1",
532
- "toolName": "test-tool-1",
533
- "type": "tool-input-start",
534
- },
535
- {
536
- "delta": "{"value1":",
537
- "id": "tool-use-id-1",
538
- "type": "tool-input-delta",
539
- },
540
- {
541
- "id": "tool-use-id-2",
542
- "toolName": "test-tool-2",
543
- "type": "tool-input-start",
544
- },
545
- {
546
- "delta": "{"value2":",
547
- "id": "tool-use-id-2",
548
- "type": "tool-input-delta",
549
- },
550
- {
551
- "delta": ""Sparkle Day"}",
552
- "id": "tool-use-id-2",
553
- "type": "tool-input-delta",
554
- },
555
- {
556
- "delta": ""Sparkle Day"}",
557
- "id": "tool-use-id-1",
558
- "type": "tool-input-delta",
559
- },
560
- {
561
- "id": "tool-use-id-1",
562
- "type": "tool-input-end",
563
- },
564
- {
565
- "input": "{"value1":"Sparkle Day"}",
566
- "toolCallId": "tool-use-id-1",
567
- "toolName": "test-tool-1",
568
- "type": "tool-call",
569
- },
570
- {
571
- "id": "tool-use-id-2",
572
- "type": "tool-input-end",
573
- },
574
- {
575
- "input": "{"value2":"Sparkle Day"}",
576
- "toolCallId": "tool-use-id-2",
577
- "toolName": "test-tool-2",
578
- "type": "tool-call",
579
- },
580
- {
581
- "finishReason": {
582
- "raw": "tool_use",
583
- "unified": "tool-calls",
584
- },
585
- "type": "finish",
586
- "usage": {
587
- "inputTokens": {
588
- "cacheRead": undefined,
589
- "cacheWrite": undefined,
590
- "noCache": undefined,
591
- "total": undefined,
592
- },
593
- "outputTokens": {
594
- "reasoning": undefined,
595
- "text": undefined,
596
- "total": undefined,
597
- },
598
- "raw": undefined,
599
- },
600
- },
601
- ]
602
- `);
603
- });
604
-
605
- it('should handle error stream parts', async () => {
606
- setupMockEventStreamHandler();
607
- server.urls[streamUrl].response = {
608
- type: 'stream-chunks',
609
- chunks: [
610
- JSON.stringify({
611
- internalServerException: {
612
- message: 'Internal Server Error',
613
- name: 'InternalServerException',
614
- $fault: 'server',
615
- $metadata: {},
616
- },
617
- }) + '\n',
618
- ],
619
- };
620
-
621
- const { stream } = await model.doStream({
622
- prompt: TEST_PROMPT,
623
- includeRawChunks: false,
624
- });
625
-
626
- expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
627
- [
628
- {
629
- "type": "stream-start",
630
- "warnings": [],
631
- },
632
- {
633
- "error": {
634
- "$fault": "server",
635
- "$metadata": {},
636
- "message": "Internal Server Error",
637
- "name": "InternalServerException",
638
- },
639
- "type": "error",
640
- },
641
- {
642
- "finishReason": {
643
- "raw": undefined,
644
- "unified": "error",
645
- },
646
- "type": "finish",
647
- "usage": {
648
- "inputTokens": {
649
- "cacheRead": undefined,
650
- "cacheWrite": undefined,
651
- "noCache": undefined,
652
- "total": undefined,
653
- },
654
- "outputTokens": {
655
- "reasoning": undefined,
656
- "text": undefined,
657
- "total": undefined,
658
- },
659
- "raw": undefined,
660
- },
661
- },
662
- ]
663
- `);
664
- });
665
-
666
- it('should handle modelStreamErrorException error', async () => {
667
- setupMockEventStreamHandler();
668
- server.urls[streamUrl].response = {
669
- type: 'stream-chunks',
670
- chunks: [
671
- JSON.stringify({
672
- modelStreamErrorException: {
673
- message: 'Model Stream Error',
674
- name: 'ModelStreamErrorException',
675
- $fault: 'server',
676
- $metadata: {},
677
- },
678
- }) + '\n',
679
- ],
680
- };
681
-
682
- const { stream } = await model.doStream({
683
- prompt: TEST_PROMPT,
684
- includeRawChunks: false,
685
- });
686
-
687
- expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
688
- [
689
- {
690
- "type": "stream-start",
691
- "warnings": [],
692
- },
693
- {
694
- "error": {
695
- "$fault": "server",
696
- "$metadata": {},
697
- "message": "Model Stream Error",
698
- "name": "ModelStreamErrorException",
699
- },
700
- "type": "error",
701
- },
702
- {
703
- "finishReason": {
704
- "raw": undefined,
705
- "unified": "error",
706
- },
707
- "type": "finish",
708
- "usage": {
709
- "inputTokens": {
710
- "cacheRead": undefined,
711
- "cacheWrite": undefined,
712
- "noCache": undefined,
713
- "total": undefined,
714
- },
715
- "outputTokens": {
716
- "reasoning": undefined,
717
- "text": undefined,
718
- "total": undefined,
719
- },
720
- "raw": undefined,
721
- },
722
- },
723
- ]
724
- `);
725
- });
726
-
727
- it('should handle throttlingException error', async () => {
728
- setupMockEventStreamHandler();
729
- server.urls[streamUrl].response = {
730
- type: 'stream-chunks',
731
- chunks: [
732
- JSON.stringify({
733
- throttlingException: {
734
- message: 'Throttling Error',
735
- name: 'ThrottlingException',
736
- $fault: 'server',
737
- $metadata: {},
738
- },
739
- }) + '\n',
740
- ],
741
- };
742
-
743
- const { stream } = await model.doStream({
744
- prompt: TEST_PROMPT,
745
- includeRawChunks: false,
746
- });
747
-
748
- expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
749
- [
750
- {
751
- "type": "stream-start",
752
- "warnings": [],
753
- },
754
- {
755
- "error": {
756
- "$fault": "server",
757
- "$metadata": {},
758
- "message": "Throttling Error",
759
- "name": "ThrottlingException",
760
- },
761
- "type": "error",
762
- },
763
- {
764
- "finishReason": {
765
- "raw": undefined,
766
- "unified": "error",
767
- },
768
- "type": "finish",
769
- "usage": {
770
- "inputTokens": {
771
- "cacheRead": undefined,
772
- "cacheWrite": undefined,
773
- "noCache": undefined,
774
- "total": undefined,
775
- },
776
- "outputTokens": {
777
- "reasoning": undefined,
778
- "text": undefined,
779
- "total": undefined,
780
- },
781
- "raw": undefined,
782
- },
783
- },
784
- ]
785
- `);
786
- });
787
-
788
- it('should handle validationException error', async () => {
789
- setupMockEventStreamHandler();
790
- server.urls[streamUrl].response = {
791
- type: 'stream-chunks',
792
- chunks: [
793
- JSON.stringify({
794
- validationException: {
795
- message: 'Validation Error',
796
- name: 'ValidationException',
797
- $fault: 'server',
798
- $metadata: {},
799
- },
800
- }) + '\n',
801
- ],
802
- };
803
-
804
- const { stream } = await model.doStream({
805
- prompt: TEST_PROMPT,
806
- includeRawChunks: false,
807
- });
808
-
809
- expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
810
- [
811
- {
812
- "type": "stream-start",
813
- "warnings": [],
814
- },
815
- {
816
- "error": {
817
- "$fault": "server",
818
- "$metadata": {},
819
- "message": "Validation Error",
820
- "name": "ValidationException",
821
- },
822
- "type": "error",
823
- },
824
- {
825
- "finishReason": {
826
- "raw": undefined,
827
- "unified": "error",
828
- },
829
- "type": "finish",
830
- "usage": {
831
- "inputTokens": {
832
- "cacheRead": undefined,
833
- "cacheWrite": undefined,
834
- "noCache": undefined,
835
- "total": undefined,
836
- },
837
- "outputTokens": {
838
- "reasoning": undefined,
839
- "text": undefined,
840
- "total": undefined,
841
- },
842
- "raw": undefined,
843
- },
844
- },
845
- ]
846
- `);
847
- });
848
-
849
- it('should handle failed chunk parsing', async () => {
850
- setupMockEventStreamHandler({
851
- success: false,
852
- errorValue: { message: 'Chunk Parsing Failed' },
853
- });
854
-
855
- const { stream } = await model.doStream({
856
- prompt: TEST_PROMPT,
857
- includeRawChunks: false,
858
- });
859
-
860
- expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
861
- [
862
- {
863
- "type": "stream-start",
864
- "warnings": [],
865
- },
866
- {
867
- "error": {
868
- "message": "Chunk Parsing Failed",
869
- },
870
- "type": "error",
871
- },
872
- {
873
- "finishReason": {
874
- "raw": undefined,
875
- "unified": "error",
876
- },
877
- "type": "finish",
878
- "usage": {
879
- "inputTokens": {
880
- "cacheRead": undefined,
881
- "cacheWrite": undefined,
882
- "noCache": undefined,
883
- "total": undefined,
884
- },
885
- "outputTokens": {
886
- "reasoning": undefined,
887
- "text": undefined,
888
- "total": undefined,
889
- },
890
- "raw": undefined,
891
- },
892
- },
893
- ]
894
- `);
895
- });
896
-
897
- it('should pass the messages and the model', async () => {
898
- setupMockEventStreamHandler();
899
- server.urls[streamUrl].response = {
900
- type: 'stream-chunks',
901
- chunks: [],
902
- };
903
-
904
- await model.doStream({
905
- prompt: TEST_PROMPT,
906
- includeRawChunks: false,
907
- });
908
-
909
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
910
- messages: [{ role: 'user', content: [{ text: 'Hello' }] }],
911
- system: [{ text: 'System Prompt' }],
912
- additionalModelResponseFieldPaths: ['/delta/stop_sequence'],
913
- });
914
- });
915
-
916
- it('should support guardrails', async () => {
917
- setupMockEventStreamHandler();
918
- server.urls[streamUrl].response = {
919
- type: 'stream-chunks',
920
- chunks: [],
921
- };
922
-
923
- await model.doStream({
924
- prompt: TEST_PROMPT,
925
- includeRawChunks: false,
926
- providerOptions: {
927
- bedrock: {
928
- guardrailConfig: {
929
- guardrailIdentifier: '-1',
930
- guardrailVersion: '1',
931
- trace: 'enabled',
932
- streamProcessingMode: 'async',
933
- },
934
- },
935
- },
936
- });
937
-
938
- expect(await server.calls[0].requestBodyJson).toMatchObject({
939
- messages: [{ role: 'user', content: [{ text: 'Hello' }] }],
940
- system: [{ text: 'System Prompt' }],
941
- guardrailConfig: {
942
- guardrailIdentifier: '-1',
943
- guardrailVersion: '1',
944
- trace: 'enabled',
945
- streamProcessingMode: 'async',
946
- },
947
- });
948
- });
949
-
950
- it('should include trace information in providerMetadata', async () => {
951
- setupMockEventStreamHandler();
952
- server.urls[streamUrl].response = {
953
- type: 'stream-chunks',
954
- chunks: [
955
- JSON.stringify({
956
- contentBlockDelta: {
957
- contentBlockIndex: 0,
958
- delta: { text: 'Hello' },
959
- },
960
- }) + '\n',
961
- JSON.stringify({
962
- metadata: {
963
- usage: { inputTokens: 4, outputTokens: 34, totalTokens: 38 },
964
- metrics: { latencyMs: 10 },
965
- trace: mockTrace,
966
- },
967
- }) + '\n',
968
- JSON.stringify({
969
- messageStop: {
970
- stopReason: 'stop_sequence',
971
- },
972
- }) + '\n',
973
- ],
974
- };
975
-
976
- const { stream } = await model.doStream({
977
- prompt: TEST_PROMPT,
978
- includeRawChunks: false,
979
- });
980
-
981
- expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
982
- [
983
- {
984
- "type": "stream-start",
985
- "warnings": [],
986
- },
987
- {
988
- "id": "0",
989
- "type": "text-start",
990
- },
991
- {
992
- "delta": "Hello",
993
- "id": "0",
994
- "type": "text-delta",
995
- },
996
- {
997
- "finishReason": {
998
- "raw": "stop_sequence",
999
- "unified": "stop",
1000
- },
1001
- "providerMetadata": {
1002
- "bedrock": {
1003
- "trace": {
1004
- "guardrail": {
1005
- "inputAssessment": {
1006
- "1abcd2ef34gh": {
1007
- "contentPolicy": {
1008
- "filters": [
1009
- {
1010
- "action": "BLOCKED",
1011
- "confidence": "LOW",
1012
- "type": "INSULTS",
1013
- },
1014
- ],
1015
- },
1016
- "wordPolicy": {
1017
- "managedWordLists": [
1018
- {
1019
- "action": "BLOCKED",
1020
- "match": "<rude word>",
1021
- "type": "PROFANITY",
1022
- },
1023
- ],
1024
- },
1025
- },
1026
- },
1027
- },
1028
- },
1029
- },
1030
- },
1031
- "type": "finish",
1032
- "usage": {
1033
- "inputTokens": {
1034
- "cacheRead": 0,
1035
- "cacheWrite": 0,
1036
- "noCache": 4,
1037
- "total": 4,
1038
- },
1039
- "outputTokens": {
1040
- "reasoning": undefined,
1041
- "text": 34,
1042
- "total": 34,
1043
- },
1044
- "raw": {
1045
- "inputTokens": 4,
1046
- "outputTokens": 34,
1047
- "totalTokens": 38,
1048
- },
1049
- },
1050
- },
1051
- ]
1052
- `);
1053
- });
1054
-
1055
- it('should include stop_sequence in provider metadata', async () => {
1056
- setupMockEventStreamHandler();
1057
- server.urls[streamUrl].response = {
1058
- type: 'stream-chunks',
1059
- chunks: [
1060
- JSON.stringify({
1061
- contentBlockDelta: {
1062
- contentBlockIndex: 0,
1063
- delta: { text: 'Hello' },
1064
- },
1065
- }) + '\n',
1066
- JSON.stringify({
1067
- metadata: {
1068
- usage: { inputTokens: 4, outputTokens: 34, totalTokens: 38 },
1069
- },
1070
- }) + '\n',
1071
- JSON.stringify({
1072
- messageStop: {
1073
- stopReason: 'stop_sequence',
1074
- additionalModelResponseFields: { delta: { stop_sequence: 'STOP' } },
1075
- },
1076
- }) + '\n',
1077
- ],
1078
- };
1079
-
1080
- const { stream } = await model.doStream({
1081
- prompt: TEST_PROMPT,
1082
- stopSequences: ['STOP'],
1083
- });
1084
-
1085
- const chunks = await convertReadableStreamToArray(stream);
1086
-
1087
- expect(chunks.filter(chunk => chunk.type === 'finish'))
1088
- .toMatchInlineSnapshot(`
1089
- [
1090
- {
1091
- "finishReason": {
1092
- "raw": "stop_sequence",
1093
- "unified": "stop",
1094
- },
1095
- "providerMetadata": {
1096
- "bedrock": {
1097
- "stopSequence": "STOP",
1098
- },
1099
- },
1100
- "type": "finish",
1101
- "usage": {
1102
- "inputTokens": {
1103
- "cacheRead": 0,
1104
- "cacheWrite": 0,
1105
- "noCache": 4,
1106
- "total": 4,
1107
- },
1108
- "outputTokens": {
1109
- "reasoning": undefined,
1110
- "text": 34,
1111
- "total": 34,
1112
- },
1113
- "raw": {
1114
- "inputTokens": 4,
1115
- "outputTokens": 34,
1116
- "totalTokens": 38,
1117
- },
1118
- },
1119
- },
1120
- ]
1121
- `);
1122
- });
1123
-
1124
- it('should correctly parse delta.stop_sequence structure in streaming with additional fields', async () => {
1125
- setupMockEventStreamHandler();
1126
- server.urls[streamUrl].response = {
1127
- type: 'stream-chunks',
1128
- chunks: [
1129
- JSON.stringify({
1130
- contentBlockDelta: {
1131
- contentBlockIndex: 0,
1132
- delta: { text: 'Response' },
1133
- },
1134
- }) + '\n',
1135
- JSON.stringify({
1136
- metadata: {
1137
- usage: { inputTokens: 5, outputTokens: 10, totalTokens: 15 },
1138
- },
1139
- }) + '\n',
1140
- JSON.stringify({
1141
- messageStop: {
1142
- stopReason: 'stop_sequence',
1143
- additionalModelResponseFields: {
1144
- delta: {
1145
- stop_sequence: 'CUSTOM_END',
1146
- },
1147
- // Additional fields that might be present
1148
- otherField: 'value',
1149
- },
1150
- },
1151
- }) + '\n',
1152
- ],
1153
- };
1154
-
1155
- const { stream } = await model.doStream({
1156
- prompt: TEST_PROMPT,
1157
- stopSequences: ['CUSTOM_END'],
1158
- });
1159
-
1160
- const chunks = await convertReadableStreamToArray(stream);
1161
- const finishChunk = chunks.find(chunk => chunk.type === 'finish');
1162
-
1163
- expect(finishChunk?.providerMetadata?.bedrock?.stopSequence).toBe(
1164
- 'CUSTOM_END',
1165
- );
1166
- expect(finishChunk?.finishReason).toEqual({
1167
- unified: 'stop',
1168
- raw: 'stop_sequence',
1169
- });
1170
- });
1171
-
1172
- it('should include response headers in rawResponse', async () => {
1173
- setupMockEventStreamHandler();
1174
- server.urls[streamUrl].response = {
1175
- type: 'stream-chunks',
1176
- headers: {
1177
- 'x-amzn-requestid': 'test-request-id',
1178
- 'x-amzn-trace-id': 'test-trace-id',
1179
- },
1180
- chunks: [
1181
- JSON.stringify({
1182
- contentBlockDelta: {
1183
- contentBlockIndex: 0,
1184
- delta: { text: 'Hello' },
1185
- },
1186
- }) + '\n',
1187
- JSON.stringify({
1188
- messageStop: {
1189
- stopReason: 'stop_sequence',
1190
- },
1191
- }) + '\n',
1192
- ],
1193
- };
1194
-
1195
- const result = await model.doStream({
1196
- prompt: TEST_PROMPT,
1197
- includeRawChunks: false,
1198
- });
1199
-
1200
- expect(result.response?.headers).toEqual({
1201
- 'cache-control': 'no-cache',
1202
- connection: 'keep-alive',
1203
- 'content-type': 'text/event-stream',
1204
- 'x-amzn-requestid': 'test-request-id',
1205
- 'x-amzn-trace-id': 'test-trace-id',
1206
- });
1207
- });
1208
-
1209
- it('should properly combine headers from all sources', async () => {
1210
- setupMockEventStreamHandler();
1211
- server.urls[streamUrl].response = {
1212
- type: 'stream-chunks',
1213
- headers: {
1214
- 'x-amzn-requestid': 'test-request-id',
1215
- 'x-amzn-trace-id': 'test-trace-id',
1216
- },
1217
- chunks: [
1218
- JSON.stringify({
1219
- contentBlockDelta: {
1220
- contentBlockIndex: 0,
1221
- delta: { text: 'Hello' },
1222
- },
1223
- }) + '\n',
1224
- JSON.stringify({
1225
- messageStop: {
1226
- stopReason: 'stop_sequence',
1227
- },
1228
- }) + '\n',
1229
- ],
1230
- };
1231
-
1232
- const optionsHeaders = {
1233
- 'options-header': 'options-value',
1234
- 'shared-header': 'options-shared',
1235
- };
1236
-
1237
- const model = new BedrockChatLanguageModel(modelId, {
1238
- baseUrl: () => baseUrl,
1239
- headers: {
1240
- 'model-header': 'model-value',
1241
- 'shared-header': 'model-shared',
1242
- },
1243
- fetch: injectFetchHeaders({
1244
- 'options-header': 'options-value',
1245
- 'model-header': 'model-value',
1246
- 'shared-header': 'options-shared',
1247
- 'signed-header': 'signed-value',
1248
- authorization: 'AWS4-HMAC-SHA256...',
1249
- }),
1250
- generateId: () => 'test-id',
1251
- });
1252
-
1253
- await model.doStream({
1254
- prompt: TEST_PROMPT,
1255
- includeRawChunks: false,
1256
- headers: optionsHeaders,
1257
- });
1258
-
1259
- const requestHeaders = server.calls[0].requestHeaders;
1260
- expect(requestHeaders['options-header']).toBe('options-value');
1261
- expect(requestHeaders['model-header']).toBe('model-value');
1262
- expect(requestHeaders['signed-header']).toBe('signed-value');
1263
- expect(requestHeaders['authorization']).toBe('AWS4-HMAC-SHA256...');
1264
- expect(requestHeaders['shared-header']).toBe('options-shared');
1265
- });
1266
-
1267
- it('should work with partial headers', async () => {
1268
- setupMockEventStreamHandler();
1269
- const model = new BedrockChatLanguageModel(modelId, {
1270
- baseUrl: () => baseUrl,
1271
- headers: {
1272
- 'model-header': 'model-value',
1273
- },
1274
- fetch: injectFetchHeaders({
1275
- 'model-header': 'model-value',
1276
- 'signed-header': 'signed-value',
1277
- authorization: 'AWS4-HMAC-SHA256...',
1278
- }),
1279
- generateId: () => 'test-id',
1280
- });
1281
-
1282
- await model.doStream({
1283
- prompt: TEST_PROMPT,
1284
- includeRawChunks: false,
1285
- });
1286
-
1287
- const requestHeaders = server.calls[0].requestHeaders;
1288
- expect(requestHeaders['model-header']).toBe('model-value');
1289
- expect(requestHeaders['signed-header']).toBe('signed-value');
1290
- expect(requestHeaders['authorization']).toBe('AWS4-HMAC-SHA256...');
1291
- });
1292
-
1293
- it('should include providerOptions in the request for streaming calls', async () => {
1294
- setupMockEventStreamHandler();
1295
- server.urls[streamUrl].response = {
1296
- type: 'stream-chunks',
1297
- chunks: [
1298
- JSON.stringify({
1299
- contentBlockDelta: {
1300
- contentBlockIndex: 0,
1301
- delta: { text: 'Dummy' },
1302
- },
1303
- }) + '\n',
1304
- JSON.stringify({
1305
- messageStop: { stopReason: 'stop_sequence' },
1306
- }) + '\n',
1307
- ],
1308
- };
1309
-
1310
- await model.doStream({
1311
- prompt: TEST_PROMPT,
1312
- includeRawChunks: false,
1313
- providerOptions: {
1314
- bedrock: {
1315
- foo: 'bar',
1316
- },
1317
- },
1318
- });
1319
-
1320
- // Verify the outgoing request body includes "foo" at the top level.
1321
- const body = await server.calls[0].requestBodyJson;
1322
- expect(body).toMatchObject({ foo: 'bar' });
1323
- });
1324
-
1325
- it('should include cache token usage in providerMetadata', async () => {
1326
- setupMockEventStreamHandler();
1327
- server.urls[streamUrl].response = {
1328
- type: 'stream-chunks',
1329
- chunks: [
1330
- JSON.stringify({
1331
- contentBlockDelta: {
1332
- contentBlockIndex: 0,
1333
- delta: { text: 'Hello' },
1334
- },
1335
- }) + '\n',
1336
- JSON.stringify({
1337
- metadata: {
1338
- usage: {
1339
- inputTokens: 4,
1340
- outputTokens: 34,
1341
- totalTokens: 38,
1342
- cacheReadInputTokens: 2,
1343
- cacheWriteInputTokens: 3,
1344
- },
1345
- },
1346
- }) + '\n',
1347
- JSON.stringify({
1348
- messageStop: {
1349
- stopReason: 'stop_sequence',
1350
- },
1351
- }) + '\n',
1352
- ],
1353
- };
1354
-
1355
- const { stream } = await model.doStream({
1356
- prompt: TEST_PROMPT,
1357
- includeRawChunks: false,
1358
- });
1359
-
1360
- expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
1361
- [
1362
- {
1363
- "type": "stream-start",
1364
- "warnings": [],
1365
- },
1366
- {
1367
- "id": "0",
1368
- "type": "text-start",
1369
- },
1370
- {
1371
- "delta": "Hello",
1372
- "id": "0",
1373
- "type": "text-delta",
1374
- },
1375
- {
1376
- "finishReason": {
1377
- "raw": "stop_sequence",
1378
- "unified": "stop",
1379
- },
1380
- "providerMetadata": {
1381
- "bedrock": {
1382
- "usage": {
1383
- "cacheWriteInputTokens": 3,
1384
- },
1385
- },
1386
- },
1387
- "type": "finish",
1388
- "usage": {
1389
- "inputTokens": {
1390
- "cacheRead": 2,
1391
- "cacheWrite": 3,
1392
- "noCache": 4,
1393
- "total": 9,
1394
- },
1395
- "outputTokens": {
1396
- "reasoning": undefined,
1397
- "text": 34,
1398
- "total": 34,
1399
- },
1400
- "raw": {
1401
- "cacheReadInputTokens": 2,
1402
- "cacheWriteInputTokens": 3,
1403
- "inputTokens": 4,
1404
- "outputTokens": 34,
1405
- "totalTokens": 38,
1406
- },
1407
- },
1408
- },
1409
- ]
1410
- `);
1411
- });
1412
-
1413
- it('should handle system messages with cache points', async () => {
1414
- setupMockEventStreamHandler();
1415
- server.urls[streamUrl].response = {
1416
- type: 'stream-chunks',
1417
- chunks: [
1418
- JSON.stringify({
1419
- contentBlockDelta: {
1420
- contentBlockIndex: 0,
1421
- delta: { text: 'Hello' },
1422
- },
1423
- }) + '\n',
1424
- JSON.stringify({
1425
- messageStop: {
1426
- stopReason: 'stop_sequence',
1427
- },
1428
- }) + '\n',
1429
- ],
1430
- };
1431
-
1432
- await model.doStream({
1433
- prompt: [
1434
- {
1435
- role: 'system',
1436
- content: 'System Prompt',
1437
- providerOptions: { bedrock: { cachePoint: { type: 'default' } } },
1438
- },
1439
- { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
1440
- ],
1441
- includeRawChunks: false,
1442
- });
1443
-
1444
- expect(await server.calls[0].requestBodyJson).toMatchObject({
1445
- system: [{ text: 'System Prompt' }, { cachePoint: { type: 'default' } }],
1446
- messages: [{ role: 'user', content: [{ text: 'Hello' }] }],
1447
- });
1448
- });
1449
-
1450
- it('should stream reasoning text deltas', async () => {
1451
- setupMockEventStreamHandler();
1452
- server.urls[streamUrl].response = {
1453
- type: 'stream-chunks',
1454
- chunks: [
1455
- JSON.stringify({
1456
- contentBlockDelta: {
1457
- contentBlockIndex: 0,
1458
- delta: {
1459
- reasoningContent: { text: 'I am thinking' },
1460
- },
1461
- },
1462
- }) + '\n',
1463
- JSON.stringify({
1464
- contentBlockDelta: {
1465
- contentBlockIndex: 0,
1466
- delta: {
1467
- reasoningContent: { text: ' about this problem...' },
1468
- },
1469
- },
1470
- }) + '\n',
1471
- JSON.stringify({
1472
- contentBlockDelta: {
1473
- contentBlockIndex: 0,
1474
- delta: {
1475
- reasoningContent: { signature: 'abc123signature' },
1476
- },
1477
- },
1478
- }) + '\n',
1479
- JSON.stringify({
1480
- contentBlockDelta: {
1481
- contentBlockIndex: 1,
1482
- delta: { text: 'Based on my reasoning, the answer is 42.' },
1483
- },
1484
- }) + '\n',
1485
- JSON.stringify({
1486
- messageStop: {
1487
- stopReason: 'stop_sequence',
1488
- },
1489
- }) + '\n',
1490
- ],
1491
- };
1492
-
1493
- const { stream } = await model.doStream({
1494
- prompt: TEST_PROMPT,
1495
- includeRawChunks: false,
1496
- });
1497
-
1498
- expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
1499
- [
1500
- {
1501
- "type": "stream-start",
1502
- "warnings": [],
1503
- },
1504
- {
1505
- "id": "0",
1506
- "type": "reasoning-start",
1507
- },
1508
- {
1509
- "delta": "I am thinking",
1510
- "id": "0",
1511
- "type": "reasoning-delta",
1512
- },
1513
- {
1514
- "delta": " about this problem...",
1515
- "id": "0",
1516
- "type": "reasoning-delta",
1517
- },
1518
- {
1519
- "delta": "",
1520
- "id": "0",
1521
- "providerMetadata": {
1522
- "bedrock": {
1523
- "signature": "abc123signature",
1524
- },
1525
- },
1526
- "type": "reasoning-delta",
1527
- },
1528
- {
1529
- "id": "1",
1530
- "type": "text-start",
1531
- },
1532
- {
1533
- "delta": "Based on my reasoning, the answer is 42.",
1534
- "id": "1",
1535
- "type": "text-delta",
1536
- },
1537
- {
1538
- "finishReason": {
1539
- "raw": "stop_sequence",
1540
- "unified": "stop",
1541
- },
1542
- "type": "finish",
1543
- "usage": {
1544
- "inputTokens": {
1545
- "cacheRead": undefined,
1546
- "cacheWrite": undefined,
1547
- "noCache": undefined,
1548
- "total": undefined,
1549
- },
1550
- "outputTokens": {
1551
- "reasoning": undefined,
1552
- "text": undefined,
1553
- "total": undefined,
1554
- },
1555
- "raw": undefined,
1556
- },
1557
- },
1558
- ]
1559
- `);
1560
- });
1561
-
1562
- it('should stream redacted reasoning', async () => {
1563
- setupMockEventStreamHandler();
1564
- server.urls[streamUrl].response = {
1565
- type: 'stream-chunks',
1566
- chunks: [
1567
- JSON.stringify({
1568
- contentBlockDelta: {
1569
- contentBlockIndex: 0,
1570
- delta: {
1571
- reasoningContent: { data: 'redacted-reasoning-data' },
1572
- },
1573
- },
1574
- }) + '\n',
1575
- JSON.stringify({
1576
- contentBlockDelta: {
1577
- contentBlockIndex: 1,
1578
- delta: { text: 'Here is my answer.' },
1579
- },
1580
- }) + '\n',
1581
- JSON.stringify({
1582
- messageStop: {
1583
- stopReason: 'stop_sequence',
1584
- },
1585
- }) + '\n',
1586
- ],
1587
- };
1588
-
1589
- const { stream } = await model.doStream({
1590
- prompt: TEST_PROMPT,
1591
- includeRawChunks: false,
1592
- });
1593
-
1594
- expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
1595
- [
1596
- {
1597
- "type": "stream-start",
1598
- "warnings": [],
1599
- },
1600
- {
1601
- "delta": "",
1602
- "id": "0",
1603
- "providerMetadata": {
1604
- "bedrock": {
1605
- "redactedData": "redacted-reasoning-data",
1606
- },
1607
- },
1608
- "type": "reasoning-delta",
1609
- },
1610
- {
1611
- "id": "1",
1612
- "type": "text-start",
1613
- },
1614
- {
1615
- "delta": "Here is my answer.",
1616
- "id": "1",
1617
- "type": "text-delta",
1618
- },
1619
- {
1620
- "finishReason": {
1621
- "raw": "stop_sequence",
1622
- "unified": "stop",
1623
- },
1624
- "type": "finish",
1625
- "usage": {
1626
- "inputTokens": {
1627
- "cacheRead": undefined,
1628
- "cacheWrite": undefined,
1629
- "noCache": undefined,
1630
- "total": undefined,
1631
- },
1632
- "outputTokens": {
1633
- "reasoning": undefined,
1634
- "text": undefined,
1635
- "total": undefined,
1636
- },
1637
- "raw": undefined,
1638
- },
1639
- },
1640
- ]
1641
- `);
1642
- });
1643
-
1644
- it('should include raw chunks when includeRawChunks is true', async () => {
1645
- setupMockEventStreamHandler();
1646
- server.urls[streamUrl].response = {
1647
- type: 'stream-chunks',
1648
- chunks: [
1649
- JSON.stringify({
1650
- contentBlockDelta: {
1651
- contentBlockIndex: 0,
1652
- delta: { text: 'Hello' },
1653
- },
1654
- }) + '\n',
1655
- JSON.stringify({
1656
- messageStop: {
1657
- stopReason: 'stop_sequence',
1658
- },
1659
- }) + '\n',
1660
- ],
1661
- };
1662
-
1663
- const { stream } = await model.doStream({
1664
- prompt: TEST_PROMPT,
1665
- includeRawChunks: true,
1666
- });
1667
-
1668
- expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
1669
- [
1670
- {
1671
- "type": "stream-start",
1672
- "warnings": [],
1673
- },
1674
- {
1675
- "rawValue": {
1676
- "contentBlockDelta": {
1677
- "contentBlockIndex": 0,
1678
- "delta": {
1679
- "text": "Hello",
1680
- },
1681
- },
1682
- },
1683
- "type": "raw",
1684
- },
1685
- {
1686
- "id": "0",
1687
- "type": "text-start",
1688
- },
1689
- {
1690
- "delta": "Hello",
1691
- "id": "0",
1692
- "type": "text-delta",
1693
- },
1694
- {
1695
- "rawValue": {
1696
- "messageStop": {
1697
- "stopReason": "stop_sequence",
1698
- },
1699
- },
1700
- "type": "raw",
1701
- },
1702
- {
1703
- "finishReason": {
1704
- "raw": "stop_sequence",
1705
- "unified": "stop",
1706
- },
1707
- "type": "finish",
1708
- "usage": {
1709
- "inputTokens": {
1710
- "cacheRead": undefined,
1711
- "cacheWrite": undefined,
1712
- "noCache": undefined,
1713
- "total": undefined,
1714
- },
1715
- "outputTokens": {
1716
- "reasoning": undefined,
1717
- "text": undefined,
1718
- "total": undefined,
1719
- },
1720
- "raw": undefined,
1721
- },
1722
- },
1723
- ]
1724
- `);
1725
- });
1726
-
1727
- it('should transform reasoningConfig to thinking in stream requests', async () => {
1728
- setupMockEventStreamHandler();
1729
- server.urls[streamUrl].response = {
1730
- type: 'stream-chunks',
1731
- chunks: [
1732
- JSON.stringify({
1733
- contentBlockDelta: {
1734
- contentBlockIndex: 0,
1735
- delta: { text: 'Hello' },
1736
- },
1737
- }) + '\n',
1738
- JSON.stringify({
1739
- messageStop: {
1740
- stopReason: 'stop_sequence',
1741
- },
1742
- }) + '\n',
1743
- ],
1744
- };
1745
-
1746
- await model.doStream({
1747
- prompt: TEST_PROMPT,
1748
- maxOutputTokens: 100,
1749
- includeRawChunks: false,
1750
- providerOptions: {
1751
- bedrock: {
1752
- reasoningConfig: {
1753
- type: 'enabled',
1754
- budgetTokens: 2000,
1755
- },
1756
- },
1757
- },
1758
- });
1759
-
1760
- const requestBody = await server.calls[0].requestBodyJson;
1761
-
1762
- // Should contain thinking in additionalModelRequestFields
1763
- expect(requestBody).toMatchObject({
1764
- additionalModelRequestFields: {
1765
- thinking: {
1766
- type: 'enabled',
1767
- budget_tokens: 2000,
1768
- },
1769
- },
1770
- inferenceConfig: {
1771
- maxTokens: 2100,
1772
- },
1773
- });
1774
-
1775
- // Should NOT contain reasoningConfig at the top level
1776
- expect(requestBody).not.toHaveProperty('reasoningConfig');
1777
- });
1778
-
1779
- it('merges user additionalModelRequestFields with derived thinking (stream)', async () => {
1780
- setupMockEventStreamHandler();
1781
- server.urls[streamUrl].response = {
1782
- type: 'stream-chunks',
1783
- chunks: [
1784
- JSON.stringify({
1785
- messageStop: {
1786
- stopReason: 'stop_sequence',
1787
- },
1788
- }) + '\n',
1789
- ],
1790
- };
1791
-
1792
- await model.doStream({
1793
- prompt: TEST_PROMPT,
1794
- includeRawChunks: false,
1795
- providerOptions: {
1796
- bedrock: {
1797
- reasoningConfig: { type: 'enabled', budgetTokens: 500 },
1798
- additionalModelRequestFields: { foo: 'bar', custom: 42 },
1799
- },
1800
- },
1801
- });
1802
-
1803
- const body = await server.calls[0].requestBodyJson;
1804
- expect(body).not.toHaveProperty('reasoningConfig');
1805
- expect(body.additionalModelRequestFields).toMatchObject({
1806
- foo: 'bar',
1807
- custom: 42,
1808
- thinking: { type: 'enabled', budget_tokens: 500 },
1809
- });
1810
- });
1811
-
1812
- it('should handle JSON response format in streaming', async () => {
1813
- setupMockEventStreamHandler();
1814
- prepareChunksFixtureResponse('bedrock-json-tool.1');
1815
-
1816
- const { stream } = await model.doStream({
1817
- prompt: [
1818
- { role: 'user', content: [{ type: 'text', text: 'Generate JSON' }] },
1819
- ],
1820
- responseFormat: {
1821
- type: 'json',
1822
- schema: {
1823
- type: 'object',
1824
- properties: { value: { type: 'string' } },
1825
- required: ['value'],
1826
- },
1827
- },
1828
- includeRawChunks: false,
1829
- });
1830
-
1831
- expect(await convertReadableStreamToArray(stream)).toMatchInlineSnapshot(`
1832
- [
1833
- {
1834
- "type": "stream-start",
1835
- "warnings": [],
1836
- },
1837
- {
1838
- "id": "0",
1839
- "type": "text-start",
1840
- },
1841
- {
1842
- "delta": "{"value":"test"}",
1843
- "id": "0",
1844
- "type": "text-delta",
1845
- },
1846
- {
1847
- "id": "0",
1848
- "type": "text-end",
1849
- },
1850
- {
1851
- "finishReason": {
1852
- "raw": "tool_use",
1853
- "unified": "stop",
1854
- },
1855
- "providerMetadata": {
1856
- "bedrock": {
1857
- "isJsonResponseFromTool": true,
1858
- "stopSequence": null,
1859
- },
1860
- },
1861
- "type": "finish",
1862
- "usage": {
1863
- "inputTokens": {
1864
- "cacheRead": undefined,
1865
- "cacheWrite": undefined,
1866
- "noCache": undefined,
1867
- "total": undefined,
1868
- },
1869
- "outputTokens": {
1870
- "reasoning": undefined,
1871
- "text": undefined,
1872
- "total": undefined,
1873
- },
1874
- "raw": undefined,
1875
- },
1876
- },
1877
- ]
1878
- `);
1879
- });
1880
-
1881
- it('should include text content before JSON tool call in streaming', async () => {
1882
- setupMockEventStreamHandler();
1883
- prepareChunksFixtureResponse('bedrock-json-tool.2');
1884
-
1885
- const { stream } = await model.doStream({
1886
- prompt: [
1887
- { role: 'user', content: [{ type: 'text', text: 'Generate JSON' }] },
1888
- ],
1889
- responseFormat: {
1890
- type: 'json',
1891
- schema: {
1892
- type: 'object',
1893
- properties: { value: { type: 'string' } },
1894
- required: ['value'],
1895
- },
1896
- },
1897
- includeRawChunks: false,
1898
- });
1899
-
1900
- const result = await convertReadableStreamToArray(stream);
1901
-
1902
- // Text before JSON tool call should be streamed normally
1903
- expect(result).toMatchInlineSnapshot(`
1904
- [
1905
- {
1906
- "type": "stream-start",
1907
- "warnings": [],
1908
- },
1909
- {
1910
- "id": "0",
1911
- "type": "text-start",
1912
- },
1913
- {
1914
- "delta": "Let me generate",
1915
- "id": "0",
1916
- "type": "text-delta",
1917
- },
1918
- {
1919
- "delta": " that JSON for you.",
1920
- "id": "0",
1921
- "type": "text-delta",
1922
- },
1923
- {
1924
- "id": "1",
1925
- "type": "text-start",
1926
- },
1927
- {
1928
- "delta": "{"value":"test"}",
1929
- "id": "1",
1930
- "type": "text-delta",
1931
- },
1932
- {
1933
- "id": "1",
1934
- "type": "text-end",
1935
- },
1936
- {
1937
- "finishReason": {
1938
- "raw": "tool_use",
1939
- "unified": "stop",
1940
- },
1941
- "providerMetadata": {
1942
- "bedrock": {
1943
- "isJsonResponseFromTool": true,
1944
- "stopSequence": null,
1945
- },
1946
- },
1947
- "type": "finish",
1948
- "usage": {
1949
- "inputTokens": {
1950
- "cacheRead": undefined,
1951
- "cacheWrite": undefined,
1952
- "noCache": undefined,
1953
- "total": undefined,
1954
- },
1955
- "outputTokens": {
1956
- "reasoning": undefined,
1957
- "text": undefined,
1958
- "total": undefined,
1959
- },
1960
- "raw": undefined,
1961
- },
1962
- },
1963
- ]
1964
- `);
1965
- });
1966
-
1967
- it('should stream text introduction before JSON-only response', async () => {
1968
- setupMockEventStreamHandler();
1969
- prepareChunksFixtureResponse('bedrock-json-only-text-first.1');
1970
-
1971
- const { stream } = await model.doStream({
1972
- prompt: [
1973
- { role: 'user', content: [{ type: 'text', text: 'Return name data' }] },
1974
- ],
1975
- responseFormat: {
1976
- type: 'json',
1977
- schema: {
1978
- type: 'object',
1979
- properties: { name: { type: 'string' } },
1980
- required: ['name'],
1981
- additionalProperties: false,
1982
- $schema: 'http://json-schema.org/draft-07/schema#',
1983
- },
1984
- },
1985
- includeRawChunks: false,
1986
- });
1987
-
1988
- const result = await convertReadableStreamToArray(stream);
1989
-
1990
- // Should emit text events for "Here is your data:" followed by JSON output as text
1991
- expect(result).toMatchInlineSnapshot(`
1992
- [
1993
- {
1994
- "type": "stream-start",
1995
- "warnings": [],
1996
- },
1997
- {
1998
- "id": "0",
1999
- "type": "text-start",
2000
- },
2001
- {
2002
- "delta": "Here is your data:",
2003
- "id": "0",
2004
- "type": "text-delta",
2005
- },
2006
- {
2007
- "id": "0",
2008
- "type": "text-end",
2009
- },
2010
- {
2011
- "id": "1",
2012
- "type": "text-start",
2013
- },
2014
- {
2015
- "delta": "{"name":"John"}",
2016
- "id": "1",
2017
- "type": "text-delta",
2018
- },
2019
- {
2020
- "id": "1",
2021
- "type": "text-end",
2022
- },
2023
- {
2024
- "finishReason": {
2025
- "raw": "tool_use",
2026
- "unified": "stop",
2027
- },
2028
- "providerMetadata": {
2029
- "bedrock": {
2030
- "isJsonResponseFromTool": true,
2031
- "stopSequence": null,
2032
- },
2033
- },
2034
- "type": "finish",
2035
- "usage": {
2036
- "inputTokens": {
2037
- "cacheRead": 0,
2038
- "cacheWrite": 0,
2039
- "noCache": 100,
2040
- "total": 100,
2041
- },
2042
- "outputTokens": {
2043
- "reasoning": undefined,
2044
- "text": 20,
2045
- "total": 20,
2046
- },
2047
- "raw": {
2048
- "inputTokens": 100,
2049
- "outputTokens": 20,
2050
- "totalTokens": 120,
2051
- },
2052
- },
2053
- },
2054
- ]
2055
- `);
2056
- });
2057
-
2058
- it('should include multiple text blocks before JSON tool call in streaming', async () => {
2059
- setupMockEventStreamHandler();
2060
- prepareChunksFixtureResponse('bedrock-json-tool.3');
2061
-
2062
- const { stream } = await model.doStream({
2063
- prompt: [
2064
- { role: 'user', content: [{ type: 'text', text: 'Generate data' }] },
2065
- ],
2066
- responseFormat: {
2067
- type: 'json',
2068
- schema: {
2069
- type: 'object',
2070
- properties: { result: { type: 'number' } },
2071
- required: ['result'],
2072
- },
2073
- },
2074
- includeRawChunks: false,
2075
- });
2076
-
2077
- const result = await convertReadableStreamToArray(stream);
2078
-
2079
- // All text blocks before the JSON tool call should be streamed
2080
- expect(result).toMatchInlineSnapshot(`
2081
- [
2082
- {
2083
- "type": "stream-start",
2084
- "warnings": [],
2085
- },
2086
- {
2087
- "id": "0",
2088
- "type": "text-start",
2089
- },
2090
- {
2091
- "delta": "First text block.",
2092
- "id": "0",
2093
- "type": "text-delta",
2094
- },
2095
- {
2096
- "id": "1",
2097
- "type": "text-start",
2098
- },
2099
- {
2100
- "delta": "Second text block.",
2101
- "id": "1",
2102
- "type": "text-delta",
2103
- },
2104
- {
2105
- "id": "2",
2106
- "type": "text-start",
2107
- },
2108
- {
2109
- "delta": "Third block.",
2110
- "id": "2",
2111
- "type": "text-delta",
2112
- },
2113
- {
2114
- "id": "3",
2115
- "type": "text-start",
2116
- },
2117
- {
2118
- "delta": "{"result":42}",
2119
- "id": "3",
2120
- "type": "text-delta",
2121
- },
2122
- {
2123
- "id": "3",
2124
- "type": "text-end",
2125
- },
2126
- {
2127
- "finishReason": {
2128
- "raw": "tool_use",
2129
- "unified": "stop",
2130
- },
2131
- "providerMetadata": {
2132
- "bedrock": {
2133
- "isJsonResponseFromTool": true,
2134
- "stopSequence": null,
2135
- },
2136
- },
2137
- "type": "finish",
2138
- "usage": {
2139
- "inputTokens": {
2140
- "cacheRead": undefined,
2141
- "cacheWrite": undefined,
2142
- "noCache": undefined,
2143
- "total": undefined,
2144
- },
2145
- "outputTokens": {
2146
- "reasoning": undefined,
2147
- "text": undefined,
2148
- "total": undefined,
2149
- },
2150
- "raw": undefined,
2151
- },
2152
- },
2153
- ]
2154
- `);
2155
- });
2156
-
2157
- it('should handle regular tool call before JSON tool call in streaming', async () => {
2158
- setupMockEventStreamHandler();
2159
- prepareChunksFixtureResponse('bedrock-json-with-tool.1');
2160
-
2161
- const { stream } = await model.doStream({
2162
- prompt: [{ role: 'user', content: [{ type: 'text', text: 'Test' }] }],
2163
- tools: [
2164
- {
2165
- type: 'function',
2166
- name: 'get-weather',
2167
- inputSchema: {
2168
- type: 'object',
2169
- properties: { location: { type: 'string' } },
2170
- required: ['location'],
2171
- },
2172
- },
2173
- ],
2174
- responseFormat: {
2175
- type: 'json',
2176
- schema: {
2177
- type: 'object',
2178
- properties: { result: { type: 'string' } },
2179
- required: ['result'],
2180
- },
2181
- },
2182
- includeRawChunks: false,
2183
- });
2184
-
2185
- const result = await convertReadableStreamToArray(stream);
2186
-
2187
- // Should include text, regular tool, text, then JSON as text
2188
- expect(result).toMatchInlineSnapshot(`
2189
- [
2190
- {
2191
- "type": "stream-start",
2192
- "warnings": [],
2193
- },
2194
- {
2195
- "id": "0",
2196
- "type": "text-start",
2197
- },
2198
- {
2199
- "delta": "Calling a tool first.",
2200
- "id": "0",
2201
- "type": "text-delta",
2202
- },
2203
- {
2204
- "id": "tool-1",
2205
- "toolName": "get-weather",
2206
- "type": "tool-input-start",
2207
- },
2208
- {
2209
- "delta": "{"location":"SF"}",
2210
- "id": "tool-1",
2211
- "type": "tool-input-delta",
2212
- },
2213
- {
2214
- "id": "tool-1",
2215
- "type": "tool-input-end",
2216
- },
2217
- {
2218
- "input": "{"location":"SF"}",
2219
- "toolCallId": "tool-1",
2220
- "toolName": "get-weather",
2221
- "type": "tool-call",
2222
- },
2223
- {
2224
- "id": "2",
2225
- "type": "text-start",
2226
- },
2227
- {
2228
- "delta": "Now JSON.",
2229
- "id": "2",
2230
- "type": "text-delta",
2231
- },
2232
- {
2233
- "id": "3",
2234
- "type": "text-start",
2235
- },
2236
- {
2237
- "delta": "{"result":"data"}",
2238
- "id": "3",
2239
- "type": "text-delta",
2240
- },
2241
- {
2242
- "id": "3",
2243
- "type": "text-end",
2244
- },
2245
- {
2246
- "finishReason": {
2247
- "raw": "tool_use",
2248
- "unified": "stop",
2249
- },
2250
- "providerMetadata": {
2251
- "bedrock": {
2252
- "isJsonResponseFromTool": true,
2253
- "stopSequence": null,
2254
- },
2255
- },
2256
- "type": "finish",
2257
- "usage": {
2258
- "inputTokens": {
2259
- "cacheRead": undefined,
2260
- "cacheWrite": undefined,
2261
- "noCache": undefined,
2262
- "total": undefined,
2263
- },
2264
- "outputTokens": {
2265
- "reasoning": undefined,
2266
- "text": undefined,
2267
- "total": undefined,
2268
- },
2269
- "raw": undefined,
2270
- },
2271
- },
2272
- ]
2273
- `);
2274
- });
2275
-
2276
- it('should handle multiple regular tool calls before JSON tool call in streaming', async () => {
2277
- setupMockEventStreamHandler();
2278
- prepareChunksFixtureResponse('bedrock-json-with-tools.1');
2279
-
2280
- const { stream } = await model.doStream({
2281
- prompt: [{ role: 'user', content: [{ type: 'text', text: 'Test' }] }],
2282
- tools: [
2283
- {
2284
- type: 'function',
2285
- name: 'tool-a',
2286
- inputSchema: {
2287
- type: 'object',
2288
- properties: { a: { type: 'string' } },
2289
- },
2290
- },
2291
- {
2292
- type: 'function',
2293
- name: 'tool-b',
2294
- inputSchema: {
2295
- type: 'object',
2296
- properties: { b: { type: 'string' } },
2297
- },
2298
- },
2299
- ],
2300
- responseFormat: {
2301
- type: 'json',
2302
- schema: {
2303
- type: 'object',
2304
- properties: { final: { type: 'string' } },
2305
- required: ['final'],
2306
- },
2307
- },
2308
- includeRawChunks: false,
2309
- });
2310
-
2311
- const result = await convertReadableStreamToArray(stream);
2312
-
2313
- // Should preserve all content in order
2314
- expect(result).toMatchInlineSnapshot(`
2315
- [
2316
- {
2317
- "type": "stream-start",
2318
- "warnings": [],
2319
- },
2320
- {
2321
- "id": "0",
2322
- "type": "text-start",
2323
- },
2324
- {
2325
- "delta": "Multiple tools.",
2326
- "id": "0",
2327
- "type": "text-delta",
2328
- },
2329
- {
2330
- "id": "tool-1",
2331
- "toolName": "tool-a",
2332
- "type": "tool-input-start",
2333
- },
2334
- {
2335
- "delta": "{"a":"1"}",
2336
- "id": "tool-1",
2337
- "type": "tool-input-delta",
2338
- },
2339
- {
2340
- "id": "tool-1",
2341
- "type": "tool-input-end",
2342
- },
2343
- {
2344
- "input": "{"a":"1"}",
2345
- "toolCallId": "tool-1",
2346
- "toolName": "tool-a",
2347
- "type": "tool-call",
2348
- },
2349
- {
2350
- "id": "tool-2",
2351
- "toolName": "tool-b",
2352
- "type": "tool-input-start",
2353
- },
2354
- {
2355
- "delta": "{"b":"2"}",
2356
- "id": "tool-2",
2357
- "type": "tool-input-delta",
2358
- },
2359
- {
2360
- "id": "tool-2",
2361
- "type": "tool-input-end",
2362
- },
2363
- {
2364
- "input": "{"b":"2"}",
2365
- "toolCallId": "tool-2",
2366
- "toolName": "tool-b",
2367
- "type": "tool-call",
2368
- },
2369
- {
2370
- "id": "3",
2371
- "type": "text-start",
2372
- },
2373
- {
2374
- "delta": "Final JSON.",
2375
- "id": "3",
2376
- "type": "text-delta",
2377
- },
2378
- {
2379
- "id": "4",
2380
- "type": "text-start",
2381
- },
2382
- {
2383
- "delta": "{"final":"result"}",
2384
- "id": "4",
2385
- "type": "text-delta",
2386
- },
2387
- {
2388
- "id": "4",
2389
- "type": "text-end",
2390
- },
2391
- {
2392
- "finishReason": {
2393
- "raw": "tool_use",
2394
- "unified": "stop",
2395
- },
2396
- "providerMetadata": {
2397
- "bedrock": {
2398
- "isJsonResponseFromTool": true,
2399
- "stopSequence": null,
2400
- },
2401
- },
2402
- "type": "finish",
2403
- "usage": {
2404
- "inputTokens": {
2405
- "cacheRead": undefined,
2406
- "cacheWrite": undefined,
2407
- "noCache": undefined,
2408
- "total": undefined,
2409
- },
2410
- "outputTokens": {
2411
- "reasoning": undefined,
2412
- "text": undefined,
2413
- "total": undefined,
2414
- },
2415
- "raw": undefined,
2416
- },
2417
- },
2418
- ]
2419
- `);
2420
- });
2421
-
2422
- it('should stream text, then regular tool calls, with JSON response format available', async () => {
2423
- setupMockEventStreamHandler();
2424
- prepareChunksFixtureResponse(
2425
- 'bedrock-json-tool-text-then-weather-then-json.1',
2426
- );
2427
-
2428
- const { stream } = await model.doStream({
2429
- prompt: [
2430
- {
2431
- role: 'user',
2432
- content: [
2433
- {
2434
- type: 'text',
2435
- text: "What's 2+2? Also check the weather in San Francisco and London.",
2436
- },
2437
- ],
2438
- },
2439
- ],
2440
- tools: [
2441
- {
2442
- type: 'function',
2443
- name: 'weather',
2444
- inputSchema: {
2445
- type: 'object',
2446
- properties: { location: { type: 'string' } },
2447
- required: ['location'],
2448
- },
2449
- },
2450
- ],
2451
- responseFormat: {
2452
- type: 'json',
2453
- schema: {
2454
- type: 'object',
2455
- properties: {
2456
- locations: {
2457
- type: 'array',
2458
- items: {
2459
- type: 'object',
2460
- properties: {
2461
- location: { type: 'string' },
2462
- temperature: { type: 'number' },
2463
- },
2464
- },
2465
- },
2466
- },
2467
- },
2468
- },
2469
- includeRawChunks: false,
2470
- });
2471
-
2472
- const result = await convertReadableStreamToArray(stream);
2473
-
2474
- // Should show: text answer, then tool calls (not JSON tool)
2475
- expect(result).toMatchInlineSnapshot(`
2476
- [
2477
- {
2478
- "type": "stream-start",
2479
- "warnings": [],
2480
- },
2481
- {
2482
- "id": "0",
2483
- "type": "text-start",
2484
- },
2485
- {
2486
- "delta": "2 + 2 equals 4. Now let me check",
2487
- "id": "0",
2488
- "type": "text-delta",
2489
- },
2490
- {
2491
- "delta": " the weather for you.",
2492
- "id": "0",
2493
- "type": "text-delta",
2494
- },
2495
- {
2496
- "id": "0",
2497
- "type": "text-end",
2498
- },
2499
- {
2500
- "id": "weather-tool-1",
2501
- "toolName": "weather",
2502
- "type": "tool-input-start",
2503
- },
2504
- {
2505
- "delta": "{"location":",
2506
- "id": "weather-tool-1",
2507
- "type": "tool-input-delta",
2508
- },
2509
- {
2510
- "delta": ""San Francisco"}",
2511
- "id": "weather-tool-1",
2512
- "type": "tool-input-delta",
2513
- },
2514
- {
2515
- "id": "weather-tool-1",
2516
- "type": "tool-input-end",
2517
- },
2518
- {
2519
- "input": "{"location":"San Francisco"}",
2520
- "toolCallId": "weather-tool-1",
2521
- "toolName": "weather",
2522
- "type": "tool-call",
2523
- },
2524
- {
2525
- "id": "weather-tool-2",
2526
- "toolName": "weather",
2527
- "type": "tool-input-start",
2528
- },
2529
- {
2530
- "delta": "{"location":"London"}",
2531
- "id": "weather-tool-2",
2532
- "type": "tool-input-delta",
2533
- },
2534
- {
2535
- "id": "weather-tool-2",
2536
- "type": "tool-input-end",
2537
- },
2538
- {
2539
- "input": "{"location":"London"}",
2540
- "toolCallId": "weather-tool-2",
2541
- "toolName": "weather",
2542
- "type": "tool-call",
2543
- },
2544
- {
2545
- "finishReason": {
2546
- "raw": "tool_use",
2547
- "unified": "tool-calls",
2548
- },
2549
- "type": "finish",
2550
- "usage": {
2551
- "inputTokens": {
2552
- "cacheRead": 0,
2553
- "cacheWrite": 0,
2554
- "noCache": 500,
2555
- "total": 500,
2556
- },
2557
- "outputTokens": {
2558
- "reasoning": undefined,
2559
- "text": 100,
2560
- "total": 100,
2561
- },
2562
- "raw": {
2563
- "inputTokens": 500,
2564
- "outputTokens": 100,
2565
- "totalTokens": 600,
2566
- },
2567
- },
2568
- },
2569
- ]
2570
- `);
2571
- });
2572
-
2573
- it('should warn when Anthropic model receives maxReasoningEffort in stream', async () => {
2574
- setupMockEventStreamHandler();
2575
- server.urls[streamUrl].response = {
2576
- type: 'stream-chunks',
2577
- chunks: [
2578
- JSON.stringify({
2579
- messageStop: {
2580
- stopReason: 'stop_sequence',
2581
- },
2582
- }) + '\n',
2583
- ],
2584
- };
2585
-
2586
- const result = await model.doStream({
2587
- prompt: TEST_PROMPT,
2588
- includeRawChunks: false,
2589
- providerOptions: {
2590
- bedrock: {
2591
- reasoningConfig: {
2592
- type: 'enabled',
2593
- maxReasoningEffort: 'medium',
2594
- },
2595
- },
2596
- },
2597
- });
2598
-
2599
- await convertReadableStreamToArray(result.stream);
2600
-
2601
- const requestBody = await server.calls[0].requestBodyJson;
2602
- expect(
2603
- requestBody.additionalModelRequestFields?.reasoningConfig,
2604
- ).toBeUndefined();
2605
- });
2606
-
2607
- it('should support tool calls with empty input (no arguments)', async () => {
2608
- setupMockEventStreamHandler();
2609
- prepareChunksFixtureResponse('bedrock-tool-no-args');
2610
-
2611
- const { stream } = await model.doStream({
2612
- tools: [
2613
- {
2614
- type: 'function',
2615
- name: 'updateIssueList',
2616
- inputSchema: {
2617
- type: 'object',
2618
- properties: {},
2619
- required: [],
2620
- additionalProperties: false,
2621
- $schema: 'http://json-schema.org/draft-07/schema#',
2622
- },
2623
- },
2624
- ],
2625
- prompt: TEST_PROMPT,
2626
- includeRawChunks: false,
2627
- });
2628
-
2629
- const result = await convertReadableStreamToArray(stream);
2630
-
2631
- const toolCallPart = result.find(part => part.type === 'tool-call');
2632
- expect(toolCallPart).toBeDefined();
2633
- expect(toolCallPart?.input).toBe('{}');
2634
- });
2635
- });
2636
-
2637
- describe('doGenerate', () => {
2638
- function prepareJsonResponse({
2639
- content = [{ type: 'text', text: 'Hello, World!' }],
2640
- usage = {
2641
- inputTokens: 4,
2642
- outputTokens: 34,
2643
- totalTokens: 38,
2644
- cacheReadInputTokens: undefined,
2645
- cacheWriteInputTokens: undefined,
2646
- },
2647
- stopReason = 'stop_sequence',
2648
- trace,
2649
- }: {
2650
- content?: Array<
2651
- | { type: 'text'; text: string }
2652
- | { type: 'thinking'; thinking: string; signature: string }
2653
- | { type: 'tool_use'; id: string; name: string; input: unknown }
2654
- | BedrockReasoningContentBlock
2655
- | BedrockRedactedReasoningContentBlock
2656
- >;
2657
- toolCalls?: Array<{
2658
- id?: string;
2659
- name: string;
2660
- args: Record<string, unknown>;
2661
- }>;
2662
- usage?: {
2663
- inputTokens: number;
2664
- outputTokens: number;
2665
- totalTokens: number;
2666
- cacheReadInputTokens?: number;
2667
- cacheWriteInputTokens?: number;
2668
- };
2669
- stopReason?: string;
2670
- trace?: typeof mockTrace;
2671
- reasoningContent?:
2672
- | BedrockReasoningContentBlock
2673
- | BedrockRedactedReasoningContentBlock
2674
- | Array<
2675
- BedrockReasoningContentBlock | BedrockRedactedReasoningContentBlock
2676
- >;
2677
- }) {
2678
- server.urls[generateUrl].response = {
2679
- type: 'json-value',
2680
- body: {
2681
- output: {
2682
- message: {
2683
- role: 'assistant',
2684
- content,
2685
- },
2686
- },
2687
- usage,
2688
- stopReason,
2689
- ...(trace ? { trace } : {}),
2690
- },
2691
- };
2692
- }
2693
-
2694
- it('should extract text response', async () => {
2695
- prepareJsonResponse({ content: [{ type: 'text', text: 'Hello, World!' }] });
2696
-
2697
- const result = await model.doGenerate({
2698
- prompt: TEST_PROMPT,
2699
- });
2700
-
2701
- expect(result.content).toMatchInlineSnapshot(`
2702
- [
2703
- {
2704
- "text": "Hello, World!",
2705
- "type": "text",
2706
- },
2707
- ]
2708
- `);
2709
- });
2710
-
2711
- it('should extract usage', async () => {
2712
- prepareJsonResponse({
2713
- usage: { inputTokens: 4, outputTokens: 34, totalTokens: 38 },
2714
- });
2715
-
2716
- const { usage } = await model.doGenerate({
2717
- prompt: TEST_PROMPT,
2718
- });
2719
-
2720
- expect(usage).toMatchInlineSnapshot(`
2721
- {
2722
- "inputTokens": {
2723
- "cacheRead": 0,
2724
- "cacheWrite": 0,
2725
- "noCache": 4,
2726
- "total": 4,
2727
- },
2728
- "outputTokens": {
2729
- "reasoning": undefined,
2730
- "text": 34,
2731
- "total": 34,
2732
- },
2733
- "raw": {
2734
- "inputTokens": 4,
2735
- "outputTokens": 34,
2736
- "totalTokens": 38,
2737
- },
2738
- }
2739
- `);
2740
- });
2741
-
2742
- it('should extract finish reason', async () => {
2743
- prepareJsonResponse({ stopReason: 'stop_sequence' });
2744
-
2745
- const { finishReason } = await model.doGenerate({
2746
- prompt: TEST_PROMPT,
2747
- });
2748
-
2749
- expect(finishReason).toMatchInlineSnapshot(`
2750
- {
2751
- "raw": "stop_sequence",
2752
- "unified": "stop",
2753
- }
2754
- `);
2755
- });
2756
-
2757
- it('should support unknown finish reason', async () => {
2758
- prepareJsonResponse({ stopReason: 'eos' });
2759
-
2760
- const { finishReason } = await model.doGenerate({
2761
- prompt: TEST_PROMPT,
2762
- });
2763
-
2764
- expect(finishReason).toMatchInlineSnapshot(`
2765
- {
2766
- "raw": "eos",
2767
- "unified": "other",
2768
- }
2769
- `);
2770
- });
2771
-
2772
- it('should pass the model and the messages', async () => {
2773
- prepareJsonResponse({});
2774
-
2775
- await model.doGenerate({
2776
- prompt: TEST_PROMPT,
2777
- });
2778
-
2779
- expect(await server.calls[0].requestBodyJson).toStrictEqual({
2780
- messages: [{ role: 'user', content: [{ text: 'Hello' }] }],
2781
- system: [{ text: 'System Prompt' }],
2782
- additionalModelResponseFieldPaths: ['/delta/stop_sequence'],
2783
- });
2784
- });
2785
-
2786
- it('should pass settings', async () => {
2787
- prepareJsonResponse({});
2788
-
2789
- await model.doGenerate({
2790
- prompt: TEST_PROMPT,
2791
- maxOutputTokens: 100,
2792
- temperature: 0.5,
2793
- topP: 0.5,
2794
- topK: 1,
2795
- });
2796
-
2797
- expect(await server.calls[0].requestBodyJson).toMatchObject({
2798
- inferenceConfig: {
2799
- maxTokens: 100,
2800
- temperature: 0.5,
2801
- topP: 0.5,
2802
- topK: 1,
2803
- },
2804
- });
2805
- });
2806
-
2807
- it('should support guardrails', async () => {
2808
- prepareJsonResponse({});
2809
-
2810
- await model.doGenerate({
2811
- prompt: TEST_PROMPT,
2812
- providerOptions: {
2813
- bedrock: {
2814
- guardrailConfig: {
2815
- guardrailIdentifier: '-1',
2816
- guardrailVersion: '1',
2817
- trace: 'enabled',
2818
- },
2819
- },
2820
- },
2821
- });
2822
-
2823
- expect(await server.calls[0].requestBodyJson).toMatchObject({
2824
- guardrailConfig: {
2825
- guardrailIdentifier: '-1',
2826
- guardrailVersion: '1',
2827
- trace: 'enabled',
2828
- },
2829
- });
2830
- });
2831
-
2832
- it('should include trace information in providerMetadata', async () => {
2833
- prepareJsonResponse({ trace: mockTrace });
2834
-
2835
- const result = await model.doGenerate({
2836
- prompt: TEST_PROMPT,
2837
- });
2838
-
2839
- expect(result.providerMetadata?.bedrock.trace).toMatchObject(mockTrace);
2840
- });
2841
-
2842
- it('should include stop_sequence in provider metadata', async () => {
2843
- server.urls[generateUrl].response = {
2844
- type: 'json-value',
2845
- body: {
2846
- output: {
2847
- message: {
2848
- role: 'assistant',
2849
- content: [{ text: 'Hello, World!' }],
2850
- },
2851
- },
2852
- stopReason: 'stop_sequence',
2853
- additionalModelResponseFields: { delta: { stop_sequence: 'STOP' } },
2854
- usage: {
2855
- inputTokens: 4,
2856
- outputTokens: 30,
2857
- totalTokens: 34,
2858
- },
2859
- },
2860
- };
2861
-
2862
- const result = await model.doGenerate({
2863
- prompt: TEST_PROMPT,
2864
- stopSequences: ['STOP'],
2865
- });
2866
-
2867
- expect(result.providerMetadata).toMatchInlineSnapshot(`
2868
- {
2869
- "bedrock": {
2870
- "stopSequence": "STOP",
2871
- },
2872
- }
2873
- `);
2874
- });
2875
-
2876
- // https://github.com/vercel/ai/issues/11371
2877
- it('should handle stop_sequence: null when stopReason is tool_use', async () => {
2878
- server.urls[generateUrl].response = {
2879
- type: 'json-value',
2880
- body: {
2881
- output: {
2882
- message: {
2883
- role: 'assistant',
2884
- content: [
2885
- { text: "I'll query your Salesforce..." },
2886
- {
2887
- toolUse: {
2888
- toolUseId: 'tool-use-id',
2889
- name: 'querySalesforce',
2890
- input: { query: 'SELECT Id FROM Account' },
2891
- },
2892
- },
2893
- ],
2894
- },
2895
- },
2896
- stopReason: 'tool_use',
2897
- additionalModelResponseFields: { delta: { stop_sequence: null } },
2898
- usage: {
2899
- inputTokens: 10,
2900
- outputTokens: 20,
2901
- totalTokens: 30,
2902
- },
2903
- },
2904
- };
2905
-
2906
- const result = await model.doGenerate({
2907
- prompt: TEST_PROMPT,
2908
- tools: [
2909
- {
2910
- type: 'function',
2911
- name: 'querySalesforce',
2912
- description: 'Query Salesforce',
2913
- inputSchema: {
2914
- type: 'object',
2915
- properties: { query: { type: 'string' } },
2916
- required: ['query'],
2917
- additionalProperties: false,
2918
- },
2919
- },
2920
- ],
2921
- });
2922
-
2923
- expect(result.finishReason).toEqual({
2924
- unified: 'tool-calls',
2925
- raw: 'tool_use',
2926
- });
2927
- expect(result.content).toMatchInlineSnapshot(`
2928
- [
2929
- {
2930
- "text": "I'll query your Salesforce...",
2931
- "type": "text",
2932
- },
2933
- {
2934
- "input": "{"query":"SELECT Id FROM Account"}",
2935
- "toolCallId": "tool-use-id",
2936
- "toolName": "querySalesforce",
2937
- "type": "tool-call",
2938
- },
2939
- ]
2940
- `);
2941
- expect(result.providerMetadata).toMatchInlineSnapshot(`
2942
- {
2943
- "bedrock": {
2944
- "stopSequence": null,
2945
- },
2946
- }
2947
- `);
2948
- });
2949
-
2950
- it('should correctly parse delta.stop_sequence structure from additionalModelResponseFields', async () => {
2951
- server.urls[generateUrl].response = {
2952
- type: 'json-value',
2953
- body: {
2954
- output: {
2955
- message: {
2956
- role: 'assistant',
2957
- content: [{ text: 'Response text' }],
2958
- },
2959
- },
2960
- stopReason: 'stop_sequence',
2961
- additionalModelResponseFields: {
2962
- delta: {
2963
- stop_sequence: 'CUSTOM_STOP',
2964
- },
2965
- // Additional fields that might be present
2966
- otherField: 'value',
2967
- },
2968
- usage: {
2969
- inputTokens: 10,
2970
- outputTokens: 20,
2971
- totalTokens: 30,
2972
- },
2973
- },
2974
- };
2975
-
2976
- const result = await model.doGenerate({
2977
- prompt: TEST_PROMPT,
2978
- stopSequences: ['CUSTOM_STOP'],
2979
- });
2980
-
2981
- expect(result.providerMetadata?.bedrock.stopSequence).toBe('CUSTOM_STOP');
2982
- expect(result.finishReason).toEqual({
2983
- unified: 'stop',
2984
- raw: 'stop_sequence',
2985
- });
2986
- });
2987
-
2988
- it('should include response headers in rawResponse', async () => {
2989
- server.urls[generateUrl].response = {
2990
- type: 'json-value',
2991
- headers: {
2992
- 'x-amzn-requestid': 'test-request-id',
2993
- 'x-amzn-trace-id': 'test-trace-id',
2994
- },
2995
- body: {
2996
- output: {
2997
- message: {
2998
- role: 'assistant',
2999
- content: [{ text: 'Testing' }],
3000
- },
3001
- },
3002
- usage: {
3003
- inputTokens: 4,
3004
- outputTokens: 34,
3005
- totalTokens: 38,
3006
- },
3007
- stopReason: 'stop_sequence',
3008
- },
3009
- };
3010
-
3011
- const result = await model.doGenerate({
3012
- prompt: TEST_PROMPT,
3013
- });
3014
-
3015
- expect(result.response?.headers).toEqual({
3016
- 'x-amzn-requestid': 'test-request-id',
3017
- 'x-amzn-trace-id': 'test-trace-id',
3018
- 'content-type': 'application/json',
3019
- 'content-length': '164',
3020
- });
3021
- });
3022
-
3023
- it('should pass tools and tool choice correctly', async () => {
3024
- prepareJsonResponse({});
3025
-
3026
- await model.doGenerate({
3027
- tools: [
3028
- {
3029
- type: 'function',
3030
- name: 'test-tool-1',
3031
- description: 'A test tool',
3032
- inputSchema: {
3033
- type: 'object',
3034
- properties: {
3035
- param1: { type: 'string' },
3036
- param2: { type: 'number' },
3037
- },
3038
- required: ['param1'],
3039
- additionalProperties: false,
3040
- },
3041
- },
3042
- ],
3043
- toolChoice: { type: 'auto' },
3044
- prompt: TEST_PROMPT,
3045
- });
3046
-
3047
- expect(await server.calls[0].requestBodyJson).toMatchObject({
3048
- toolConfig: {
3049
- tools: [
3050
- {
3051
- toolSpec: {
3052
- name: 'test-tool-1',
3053
- description: 'A test tool',
3054
- inputSchema: {
3055
- json: {
3056
- type: 'object',
3057
- properties: {
3058
- param1: { type: 'string' },
3059
- param2: { type: 'number' },
3060
- },
3061
- required: ['param1'],
3062
- additionalProperties: false,
3063
- },
3064
- },
3065
- },
3066
- },
3067
- ],
3068
- },
3069
- });
3070
- });
3071
-
3072
- it('should omit empty tool descriptions to avoid Bedrock validation errors', async () => {
3073
- prepareJsonResponse({});
3074
-
3075
- await model.doGenerate({
3076
- tools: [
3077
- {
3078
- type: 'function',
3079
- name: 'tool-with-empty-desc',
3080
- description: '', // Empty string should be omitted
3081
- inputSchema: {
3082
- type: 'object',
3083
- properties: {
3084
- param1: { type: 'string' },
3085
- },
3086
- required: ['param1'],
3087
- additionalProperties: false,
3088
- },
3089
- },
3090
- {
3091
- type: 'function',
3092
- name: 'tool-with-whitespace-desc',
3093
- description: ' ', // Whitespace-only should be omitted
3094
- inputSchema: {
3095
- type: 'object',
3096
- properties: {
3097
- param2: { type: 'number' },
3098
- },
3099
- required: ['param2'],
3100
- additionalProperties: false,
3101
- },
3102
- },
3103
- {
3104
- type: 'function',
3105
- name: 'tool-with-valid-desc',
3106
- description: 'Valid description',
3107
- inputSchema: {
3108
- type: 'object',
3109
- properties: {
3110
- param3: { type: 'boolean' },
3111
- },
3112
- required: ['param3'],
3113
- additionalProperties: false,
3114
- },
3115
- },
3116
- ],
3117
- toolChoice: { type: 'auto' },
3118
- prompt: TEST_PROMPT,
3119
- });
3120
-
3121
- const requestBody = await server.calls[0].requestBodyJson;
3122
-
3123
- // Tool with empty description should not have description field
3124
- expect(requestBody.toolConfig.tools[0].toolSpec).not.toHaveProperty(
3125
- 'description',
3126
- );
3127
- expect(requestBody.toolConfig.tools[0].toolSpec.name).toBe(
3128
- 'tool-with-empty-desc',
3129
- );
3130
-
3131
- // Tool with whitespace-only description should not have description field
3132
- expect(requestBody.toolConfig.tools[1].toolSpec).not.toHaveProperty(
3133
- 'description',
3134
- );
3135
- expect(requestBody.toolConfig.tools[1].toolSpec.name).toBe(
3136
- 'tool-with-whitespace-desc',
3137
- );
3138
-
3139
- // Tool with valid description should have description field
3140
- expect(requestBody.toolConfig.tools[2].toolSpec.description).toBe(
3141
- 'Valid description',
3142
- );
3143
- expect(requestBody.toolConfig.tools[2].toolSpec.name).toBe(
3144
- 'tool-with-valid-desc',
3145
- );
3146
- });
3147
-
3148
- it('should handle Anthropic provider-defined tools', async () => {
3149
- mockPrepareAnthropicTools.mockReturnValue(
3150
- Promise.resolve({
3151
- tools: [
3152
- { name: 'bash', type: 'bash_20241022', cache_control: undefined },
3153
- ],
3154
- toolChoice: { type: 'auto' },
3155
- toolWarnings: [],
3156
- betas: new Set(['computer-use-2024-10-22']),
3157
- }),
3158
- );
3159
-
3160
- // Set up the mock response for this specific URL and test case
3161
- server.urls[anthropicGenerateUrl].response = {
3162
- type: 'json-value',
3163
- body: {
3164
- output: {
3165
- message: {
3166
- role: 'assistant',
3167
- content: [
3168
- {
3169
- toolUse: {
3170
- toolUseId: 'tool-use-id',
3171
- name: 'bash',
3172
- input: { command: 'ls -l' },
3173
- },
3174
- },
3175
- ],
3176
- },
3177
- },
3178
- usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
3179
- stopReason: 'tool_use',
3180
- },
3181
- };
3182
-
3183
- const anthropicModel = new BedrockChatLanguageModel(anthropicModelId, {
3184
- baseUrl: () => baseUrl,
3185
- headers: {},
3186
- // No fetch property: defaults to global fetch, which is mocked by the test server.
3187
- generateId: () => 'test-id',
3188
- });
3189
-
3190
- const result = await anthropicModel.doGenerate({
3191
- prompt: TEST_PROMPT,
3192
- tools: [
3193
- {
3194
- type: 'provider',
3195
- id: 'anthropic.bash_20241022',
3196
- name: 'bash',
3197
- args: {},
3198
- },
3199
- ],
3200
- toolChoice: { type: 'auto' },
3201
- });
3202
-
3203
- const requestBody = await server.calls[0].requestBodyJson;
3204
-
3205
- expect(requestBody.additionalModelRequestFields).toEqual({
3206
- tool_choice: { type: 'auto' },
3207
- anthropic_beta: ['computer-use-2024-10-22'],
3208
- });
3209
-
3210
- expect(requestBody.toolConfig).toBeDefined();
3211
- expect(requestBody.toolConfig.tools).toHaveLength(1);
3212
- expect(requestBody.toolConfig.tools[0].toolSpec.name).toBe('bash');
3213
- expect(requestBody.toolConfig.tools[0].toolSpec.inputSchema.json).toEqual({
3214
- $schema: 'http://json-schema.org/draft-07/schema#',
3215
- type: 'object',
3216
- properties: {
3217
- command: { type: 'string' },
3218
- restart: { type: 'boolean' },
3219
- },
3220
- required: ['command'],
3221
- additionalProperties: false,
3222
- });
3223
-
3224
- expect(result.warnings).toEqual([]);
3225
- expect(result.content).toMatchInlineSnapshot(`
3226
- [
3227
- {
3228
- "input": "{"command":"ls -l"}",
3229
- "toolCallId": "tool-use-id",
3230
- "toolName": "bash",
3231
- "type": "tool-call",
3232
- },
3233
- ]
3234
- `);
3235
- });
3236
-
3237
- it('should include anthropic_beta in additionalModelRequestFields when using extended context', async () => {
3238
- server.urls[anthropicGenerateUrl].response = {
3239
- type: 'json-value',
3240
- body: {
3241
- output: {
3242
- message: {
3243
- role: 'assistant',
3244
- content: [{ type: 'text', text: 'test response' }],
3245
- },
3246
- },
3247
- usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
3248
- stopReason: 'stop',
3249
- },
3250
- };
3251
-
3252
- const anthropicModel = new BedrockChatLanguageModel(anthropicModelId, {
3253
- baseUrl: () => baseUrl,
3254
- headers: {},
3255
- generateId: () => 'test-id',
3256
- });
3257
-
3258
- await anthropicModel.doGenerate({
3259
- prompt: TEST_PROMPT,
3260
- providerOptions: {
3261
- bedrock: {
3262
- anthropicBeta: ['context-1m-2025-08-07'],
3263
- },
3264
- },
3265
- });
3266
-
3267
- const requestBody = await server.calls[0].requestBodyJson;
3268
-
3269
- expect(requestBody.additionalModelRequestFields).toEqual({
3270
- anthropic_beta: ['context-1m-2025-08-07'],
3271
- });
3272
- });
3273
-
3274
- it('should not include anthropic-beta in HTTP headers', async () => {
3275
- server.urls[anthropicGenerateUrl].response = {
3276
- type: 'json-value',
3277
- body: {
3278
- output: {
3279
- message: {
3280
- role: 'assistant',
3281
- content: [{ type: 'text', text: 'test response' }],
3282
- },
3283
- },
3284
- usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
3285
- stopReason: 'stop',
3286
- },
3287
- };
3288
-
3289
- const anthropicModel = new BedrockChatLanguageModel(anthropicModelId, {
3290
- baseUrl: () => baseUrl,
3291
- headers: {},
3292
- generateId: () => 'test-id',
3293
- });
3294
-
3295
- await anthropicModel.doGenerate({
3296
- prompt: TEST_PROMPT,
3297
- tools: [
3298
- {
3299
- type: 'provider',
3300
- id: 'anthropic.bash_20241022',
3301
- name: 'bash',
3302
- args: {},
3303
- },
3304
- ],
3305
- });
3306
-
3307
- const requestHeaders = server.calls[0].requestHeaders;
3308
-
3309
- expect(requestHeaders['anthropic-beta']).toBeUndefined();
3310
- });
3311
-
3312
- it('should combine user-provided and tool-generated betas in body', async () => {
3313
- server.urls[anthropicGenerateUrl].response = {
3314
- type: 'json-value',
3315
- body: {
3316
- output: {
3317
- message: {
3318
- role: 'assistant',
3319
- content: [
3320
- {
3321
- type: 'tool_use',
3322
- id: 'tool-use-id',
3323
- name: 'bash',
3324
- input: { command: 'ls -l' },
3325
- },
3326
- ],
3327
- },
3328
- },
3329
- usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
3330
- stopReason: 'tool_use',
3331
- },
3332
- };
3333
-
3334
- const anthropicModel = new BedrockChatLanguageModel(anthropicModelId, {
3335
- baseUrl: () => baseUrl,
3336
- headers: {},
3337
- generateId: () => 'test-id',
3338
- });
3339
-
3340
- await anthropicModel.doGenerate({
3341
- prompt: TEST_PROMPT,
3342
- tools: [
3343
- {
3344
- type: 'provider',
3345
- id: 'anthropic.bash_20241022',
3346
- name: 'bash',
3347
- args: {},
3348
- },
3349
- ],
3350
- providerOptions: {
3351
- bedrock: {
3352
- anthropicBeta: ['context-1m-2025-08-07'],
3353
- },
3354
- },
3355
- });
3356
-
3357
- const requestBody = await server.calls[0].requestBodyJson;
3358
-
3359
- expect(requestBody.additionalModelRequestFields.anthropic_beta).toEqual([
3360
- 'context-1m-2025-08-07',
3361
- 'computer-use-2024-10-22',
3362
- ]);
3363
- });
3364
-
3365
- it('should properly combine headers from all sources', async () => {
3366
- prepareJsonResponse({});
3367
-
3368
- const optionsHeaders = {
3369
- 'options-header': 'options-value',
3370
- 'shared-header': 'options-shared',
3371
- };
3372
-
3373
- const model = new BedrockChatLanguageModel(modelId, {
3374
- baseUrl: () => baseUrl,
3375
- headers: {
3376
- 'model-header': 'model-value',
3377
- 'shared-header': 'model-shared',
3378
- },
3379
- fetch: injectFetchHeaders({
3380
- 'options-header': 'options-value',
3381
- 'model-header': 'model-value',
3382
- 'shared-header': 'options-shared',
3383
- 'signed-header': 'signed-value',
3384
- authorization: 'AWS4-HMAC-SHA256...',
3385
- }),
3386
- generateId: () => 'test-id',
3387
- });
3388
-
3389
- await model.doGenerate({
3390
- prompt: TEST_PROMPT,
3391
- headers: optionsHeaders,
3392
- });
3393
-
3394
- const requestHeaders = server.calls[0].requestHeaders;
3395
- expect(requestHeaders['options-header']).toBe('options-value');
3396
- expect(requestHeaders['model-header']).toBe('model-value');
3397
- expect(requestHeaders['signed-header']).toBe('signed-value');
3398
- expect(requestHeaders['authorization']).toBe('AWS4-HMAC-SHA256...');
3399
- expect(requestHeaders['shared-header']).toBe('options-shared');
3400
- });
3401
-
3402
- it('should work with partial headers', async () => {
3403
- prepareJsonResponse({});
3404
-
3405
- const model = new BedrockChatLanguageModel(modelId, {
3406
- baseUrl: () => baseUrl,
3407
- headers: {
3408
- 'model-header': 'model-value',
3409
- },
3410
- fetch: injectFetchHeaders({
3411
- 'model-header': 'model-value',
3412
- 'signed-header': 'signed-value',
3413
- authorization: 'AWS4-HMAC-SHA256...',
3414
- }),
3415
- generateId: () => 'test-id',
3416
- });
3417
-
3418
- await model.doGenerate({
3419
- prompt: TEST_PROMPT,
3420
- });
3421
-
3422
- const requestHeaders = server.calls[0].requestHeaders;
3423
- expect(requestHeaders['model-header']).toBe('model-value');
3424
- expect(requestHeaders['signed-header']).toBe('signed-value');
3425
- expect(requestHeaders['authorization']).toBe('AWS4-HMAC-SHA256...');
3426
- });
3427
-
3428
- it('should include providerOptions in the request for generate calls', async () => {
3429
- prepareJsonResponse({
3430
- content: [{ type: 'text', text: 'Test generation' }],
3431
- });
3432
-
3433
- await model.doGenerate({
3434
- prompt: TEST_PROMPT,
3435
- providerOptions: {
3436
- bedrock: {
3437
- foo: 'bar',
3438
- },
3439
- },
3440
- });
3441
-
3442
- // Verify that the outgoing request body includes "foo" at its top level.
3443
- const body = await server.calls[0].requestBodyJson;
3444
- expect(body).toMatchObject({ foo: 'bar' });
3445
- });
3446
-
3447
- it('should include cache token usage in providerMetadata', async () => {
3448
- prepareJsonResponse({
3449
- content: [{ type: 'text', text: 'Testing' }],
3450
- usage: {
3451
- inputTokens: 4,
3452
- outputTokens: 34,
3453
- totalTokens: 38,
3454
- cacheReadInputTokens: 2,
3455
- cacheWriteInputTokens: 3,
3456
- },
3457
- });
3458
-
3459
- const response = await model.doGenerate({
3460
- prompt: TEST_PROMPT,
3461
- });
3462
-
3463
- expect(response.providerMetadata).toMatchInlineSnapshot(`
3464
- {
3465
- "bedrock": {
3466
- "stopSequence": null,
3467
- "usage": {
3468
- "cacheWriteInputTokens": 3,
3469
- },
3470
- },
3471
- }
3472
- `);
3473
- expect(response.usage).toMatchInlineSnapshot(`
3474
- {
3475
- "inputTokens": {
3476
- "cacheRead": 2,
3477
- "cacheWrite": 3,
3478
- "noCache": 4,
3479
- "total": 9,
3480
- },
3481
- "outputTokens": {
3482
- "reasoning": undefined,
3483
- "text": 34,
3484
- "total": 34,
3485
- },
3486
- "raw": {
3487
- "cacheReadInputTokens": 2,
3488
- "cacheWriteInputTokens": 3,
3489
- "inputTokens": 4,
3490
- "outputTokens": 34,
3491
- "totalTokens": 38,
3492
- },
3493
- }
3494
- `);
3495
- });
3496
-
3497
- it('should handle system messages with cache points', async () => {
3498
- prepareJsonResponse({});
3499
-
3500
- await model.doGenerate({
3501
- prompt: [
3502
- {
3503
- role: 'system',
3504
- content: 'System Prompt',
3505
- providerOptions: { bedrock: { cachePoint: { type: 'default' } } },
3506
- },
3507
- { role: 'user', content: [{ type: 'text', text: 'Hello' }] },
3508
- ],
3509
- });
3510
-
3511
- expect(await server.calls[0].requestBodyJson).toMatchObject({
3512
- system: [{ text: 'System Prompt' }, { cachePoint: { type: 'default' } }],
3513
- messages: [{ role: 'user', content: [{ text: 'Hello' }] }],
3514
- });
3515
- });
3516
-
3517
- it('should transform reasoningConfig to thinking in additionalModelRequestFields', async () => {
3518
- prepareJsonResponse({});
3519
-
3520
- await model.doGenerate({
3521
- prompt: TEST_PROMPT,
3522
- maxOutputTokens: 100,
3523
- providerOptions: {
3524
- bedrock: {
3525
- reasoningConfig: {
3526
- type: 'enabled',
3527
- budgetTokens: 2000,
3528
- },
3529
- },
3530
- },
3531
- });
3532
-
3533
- const requestBody = await server.calls[0].requestBodyJson;
3534
-
3535
- // Should contain thinking in additionalModelRequestFields
3536
- expect(requestBody).toMatchObject({
3537
- additionalModelRequestFields: {
3538
- thinking: {
3539
- type: 'enabled',
3540
- budget_tokens: 2000,
3541
- },
3542
- },
3543
- inferenceConfig: {
3544
- maxTokens: 2100,
3545
- },
3546
- });
3547
-
3548
- // Should NOT contain reasoningConfig at the top level
3549
- expect(requestBody).not.toHaveProperty('reasoningConfig');
3550
- });
3551
-
3552
- it('merges user additionalModelRequestFields with derived thinking (generate)', async () => {
3553
- prepareJsonResponse({});
3554
-
3555
- await model.doGenerate({
3556
- prompt: TEST_PROMPT,
3557
- providerOptions: {
3558
- bedrock: {
3559
- reasoningConfig: { type: 'enabled', budgetTokens: 1234 },
3560
- additionalModelRequestFields: { foo: 'bar', custom: 42 },
3561
- },
3562
- },
3563
- });
3564
-
3565
- const body = await server.calls[0].requestBodyJson;
3566
- expect(body).not.toHaveProperty('reasoningConfig');
3567
- expect(body.additionalModelRequestFields).toMatchObject({
3568
- foo: 'bar',
3569
- custom: 42,
3570
- thinking: { type: 'enabled', budget_tokens: 1234 },
3571
- });
3572
- });
3573
-
3574
- it('maps maxReasoningEffort for Nova without thinking (generate)', async () => {
3575
- server.urls[novaGenerateUrl].response = {
3576
- type: 'json-value',
3577
- body: {
3578
- output: {
3579
- message: { content: [{ text: 'Hello' }], role: 'assistant' },
3580
- },
3581
- stopReason: 'stop_sequence',
3582
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
3583
- },
3584
- };
3585
-
3586
- await novaModel.doGenerate({
3587
- prompt: TEST_PROMPT,
3588
- providerOptions: {
3589
- bedrock: {
3590
- reasoningConfig: {
3591
- type: 'enabled',
3592
- maxReasoningEffort: 'medium',
3593
- budgetTokens: 2048,
3594
- },
3595
- },
3596
- },
3597
- });
3598
-
3599
- const requestBody = await server.calls[0].requestBodyJson;
3600
- expect(requestBody).toMatchObject({
3601
- additionalModelRequestFields: {
3602
- reasoningConfig: {
3603
- type: 'enabled',
3604
- maxReasoningEffort: 'medium',
3605
- },
3606
- },
3607
- });
3608
- expect(requestBody.additionalModelRequestFields?.thinking).toBeUndefined();
3609
- });
3610
-
3611
- it('maps maxReasoningEffort to reasoning_effort for OpenAI models (generate)', async () => {
3612
- server.urls[openaiGenerateUrl].response = {
3613
- type: 'json-value',
3614
- body: {
3615
- output: {
3616
- message: { content: [{ text: 'Hello' }], role: 'assistant' },
3617
- },
3618
- stopReason: 'stop_sequence',
3619
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
3620
- },
3621
- };
3622
-
3623
- await openaiModel.doGenerate({
3624
- prompt: TEST_PROMPT,
3625
- providerOptions: {
3626
- bedrock: {
3627
- reasoningConfig: {
3628
- maxReasoningEffort: 'medium',
3629
- },
3630
- },
3631
- },
3632
- });
3633
-
3634
- const requestBody = await server.calls[0].requestBodyJson;
3635
- expect(requestBody).toMatchObject({
3636
- additionalModelRequestFields: {
3637
- reasoning_effort: 'medium',
3638
- },
3639
- });
3640
- expect(
3641
- requestBody.additionalModelRequestFields?.reasoningConfig,
3642
- ).toBeUndefined();
3643
- expect(requestBody.additionalModelRequestFields?.thinking).toBeUndefined();
3644
- });
3645
-
3646
- it('should warn when Anthropic model receives maxReasoningEffort (generate)', async () => {
3647
- prepareJsonResponse({});
3648
-
3649
- const result = await model.doGenerate({
3650
- prompt: TEST_PROMPT,
3651
- providerOptions: {
3652
- bedrock: {
3653
- reasoningConfig: {
3654
- type: 'enabled',
3655
- maxReasoningEffort: 'medium',
3656
- },
3657
- },
3658
- },
3659
- });
3660
-
3661
- const requestBody = await server.calls[0].requestBodyJson;
3662
- expect(
3663
- requestBody.additionalModelRequestFields?.reasoningConfig,
3664
- ).toBeUndefined();
3665
-
3666
- expect(result.warnings).toContainEqual({
3667
- type: 'unsupported',
3668
- feature: 'maxReasoningEffort',
3669
- details:
3670
- 'maxReasoningEffort applies only to Amazon Nova models on Bedrock and will be ignored for this model.',
3671
- });
3672
- });
3673
-
3674
- it('should extract reasoning text with signature', async () => {
3675
- const reasoningText = 'I need to think about this problem carefully...';
3676
- const signature = 'abc123signature';
3677
-
3678
- prepareJsonResponse({
3679
- content: [
3680
- {
3681
- reasoningContent: {
3682
- reasoningText: {
3683
- text: reasoningText,
3684
- signature,
3685
- },
3686
- },
3687
- },
3688
- { type: 'text', text: 'The answer is 42.' },
3689
- ],
3690
- });
3691
-
3692
- const result = await model.doGenerate({
3693
- prompt: TEST_PROMPT,
3694
- });
3695
-
3696
- expect(result.content).toMatchInlineSnapshot(`
3697
- [
3698
- {
3699
- "providerMetadata": {
3700
- "bedrock": {
3701
- "signature": "abc123signature",
3702
- },
3703
- },
3704
- "text": "I need to think about this problem carefully...",
3705
- "type": "reasoning",
3706
- },
3707
- {
3708
- "text": "The answer is 42.",
3709
- "type": "text",
3710
- },
3711
- ]
3712
- `);
3713
- });
3714
-
3715
- it('should extract reasoning text without signature', async () => {
3716
- const reasoningText = 'I need to think about this problem carefully...';
3717
-
3718
- prepareJsonResponse({
3719
- content: [
3720
- {
3721
- reasoningContent: {
3722
- reasoningText: {
3723
- text: reasoningText,
3724
- },
3725
- },
3726
- },
3727
- { type: 'text', text: 'The answer is 42.' },
3728
- ],
3729
- });
3730
-
3731
- const result = await model.doGenerate({
3732
- prompt: TEST_PROMPT,
3733
- });
3734
-
3735
- expect(result.content).toMatchInlineSnapshot(`
3736
- [
3737
- {
3738
- "text": "I need to think about this problem carefully...",
3739
- "type": "reasoning",
3740
- },
3741
- {
3742
- "text": "The answer is 42.",
3743
- "type": "text",
3744
- },
3745
- ]
3746
- `);
3747
- });
3748
-
3749
- it('should extract redacted reasoning', async () => {
3750
- prepareJsonResponse({
3751
- content: [
3752
- {
3753
- reasoningContent: {
3754
- redactedReasoning: {
3755
- data: 'redacted-reasoning-data',
3756
- },
3757
- },
3758
- },
3759
- { type: 'text', text: 'The answer is 42.' },
3760
- ],
3761
- });
3762
-
3763
- const result = await model.doGenerate({
3764
- prompt: TEST_PROMPT,
3765
- });
3766
-
3767
- expect(result.content).toMatchInlineSnapshot(`
3768
- [
3769
- {
3770
- "providerMetadata": {
3771
- "bedrock": {
3772
- "redactedData": "redacted-reasoning-data",
3773
- },
3774
- },
3775
- "text": "",
3776
- "type": "reasoning",
3777
- },
3778
- {
3779
- "text": "The answer is 42.",
3780
- "type": "text",
3781
- },
3782
- ]
3783
- `);
3784
- });
3785
-
3786
- it('should handle multiple reasoning blocks', async () => {
3787
- prepareJsonResponse({
3788
- content: [
3789
- {
3790
- reasoningContent: {
3791
- reasoningText: {
3792
- text: 'First reasoning block',
3793
- signature: 'sig1',
3794
- },
3795
- },
3796
- },
3797
- {
3798
- reasoningContent: {
3799
- redactedReasoning: {
3800
- data: 'redacted-data',
3801
- },
3802
- },
3803
- },
3804
- { type: 'text', text: 'The answer is 42.' },
3805
- ],
3806
- });
3807
-
3808
- const result = await model.doGenerate({
3809
- prompt: TEST_PROMPT,
3810
- });
3811
-
3812
- expect(result.content).toMatchInlineSnapshot(`
3813
- [
3814
- {
3815
- "providerMetadata": {
3816
- "bedrock": {
3817
- "signature": "sig1",
3818
- },
3819
- },
3820
- "text": "First reasoning block",
3821
- "type": "reasoning",
3822
- },
3823
- {
3824
- "providerMetadata": {
3825
- "bedrock": {
3826
- "redactedData": "redacted-data",
3827
- },
3828
- },
3829
- "text": "",
3830
- "type": "reasoning",
3831
- },
3832
- {
3833
- "text": "The answer is 42.",
3834
- "type": "text",
3835
- },
3836
- ]
3837
- `);
3838
- });
3839
-
3840
- it('should omit toolConfig and filter tool content when conversation has tool calls but no active tools', async () => {
3841
- prepareJsonResponse({});
3842
-
3843
- const conversationWithToolCalls: LanguageModelV3Prompt = [
3844
- {
3845
- role: 'user',
3846
- content: [{ type: 'text', text: 'What is the weather in Toronto?' }],
3847
- },
3848
- {
3849
- role: 'assistant',
3850
- content: [
3851
- {
3852
- type: 'tool-call',
3853
- toolCallId: 'tool-call-1',
3854
- toolName: 'weather',
3855
- input: { city: 'Toronto' },
3856
- },
3857
- ],
3858
- },
3859
- {
3860
- role: 'tool',
3861
- content: [
3862
- {
3863
- type: 'tool-result',
3864
- toolCallId: 'tool-call-1',
3865
- toolName: 'weather',
3866
- output: {
3867
- type: 'text',
3868
- value: 'The weather in Toronto is 20°C.',
3869
- },
3870
- },
3871
- ],
3872
- },
3873
- {
3874
- role: 'user',
3875
- content: [{ type: 'text', text: 'Now give me a summary.' }],
3876
- },
3877
- ];
3878
-
3879
- const result = await model.doGenerate({
3880
- prompt: conversationWithToolCalls,
3881
- tools: [],
3882
- });
3883
-
3884
- const requestBody = await server.calls[0].requestBodyJson;
3885
-
3886
- expect(requestBody.toolConfig).toMatchInlineSnapshot(`undefined`);
3887
-
3888
- expect(requestBody.messages).toMatchInlineSnapshot(`
3889
- [
3890
- {
3891
- "content": [
3892
- {
3893
- "text": "What is the weather in Toronto?",
3894
- },
3895
- {
3896
- "text": "Now give me a summary.",
3897
- },
3898
- ],
3899
- "role": "user",
3900
- },
3901
- ]
3902
- `);
3903
-
3904
- expect(result.warnings).toMatchInlineSnapshot(`
3905
- [
3906
- {
3907
- "details": "Tool calls and results removed from conversation because Bedrock does not support tool content without active tools.",
3908
- "feature": "toolContent",
3909
- "type": "unsupported",
3910
- },
3911
- ]
3912
- `);
3913
- });
3914
-
3915
- it('should handle JSON response format with schema', async () => {
3916
- prepareJsonResponse({
3917
- content: [
3918
- {
3919
- toolUse: {
3920
- toolUseId: 'json-tool-id',
3921
- name: 'json',
3922
- input: {
3923
- recipe: { name: 'Lasagna', ingredients: ['pasta', 'cheese'] },
3924
- },
3925
- },
3926
- } as any,
3927
- ],
3928
- stopReason: 'tool_use',
3929
- });
3930
-
3931
- const result = await model.doGenerate({
3932
- prompt: [
3933
- {
3934
- role: 'user',
3935
- content: [{ type: 'text', text: 'Generate a recipe' }],
3936
- },
3937
- ],
3938
- responseFormat: {
3939
- type: 'json',
3940
- schema: {
3941
- type: 'object',
3942
- properties: {
3943
- recipe: {
3944
- type: 'object',
3945
- properties: {
3946
- name: { type: 'string' },
3947
- ingredients: { type: 'array', items: { type: 'string' } },
3948
- },
3949
- required: ['name', 'ingredients'],
3950
- },
3951
- },
3952
- required: ['recipe'],
3953
- },
3954
- },
3955
- });
3956
-
3957
- expect(result.content).toMatchInlineSnapshot(`
3958
- [
3959
- {
3960
- "text": "{"recipe":{"name":"Lasagna","ingredients":["pasta","cheese"]}}",
3961
- "type": "text",
3962
- },
3963
- ]
3964
- `);
3965
-
3966
- expect(result.providerMetadata?.bedrock?.isJsonResponseFromTool).toBe(true);
3967
- expect(result.finishReason).toMatchInlineSnapshot(`
3968
- {
3969
- "raw": "tool_use",
3970
- "unified": "stop",
3971
- }
3972
- `);
3973
-
3974
- const requestBody = await server.calls[0].requestBodyJson;
3975
- expect(requestBody.toolConfig.tools).toHaveLength(1);
3976
- expect(requestBody.toolConfig.tools[0].toolSpec.name).toBe('json');
3977
- expect(requestBody.toolConfig.tools[0].toolSpec.description).toBe(
3978
- 'Respond with a JSON object.',
3979
- );
3980
- expect(requestBody.toolConfig.toolChoice).toEqual({ any: {} });
3981
- });
3982
-
3983
- describe('json schema response format with json tool response', () => {
3984
- let result: Awaited<ReturnType<typeof model.doGenerate>>;
3985
-
3986
- beforeEach(async () => {
3987
- prepareJsonFixtureResponse('bedrock-json-tool.1');
3988
-
3989
- result = await model.doGenerate({
3990
- prompt: [{ role: 'user', content: [{ type: 'text', text: 'Test' }] }],
3991
- responseFormat: {
3992
- type: 'json',
3993
- schema: {
3994
- type: 'object',
3995
- properties: {
3996
- elements: {
3997
- type: 'array',
3998
- items: {
3999
- type: 'object',
4000
- properties: {
4001
- location: { type: 'string' },
4002
- temperature: { type: 'number' },
4003
- condition: { type: 'string' },
4004
- },
4005
- required: ['location', 'temperature', 'condition'],
4006
- },
4007
- },
4008
- },
4009
- required: ['elements'],
4010
- additionalProperties: false,
4011
- $schema: 'http://json-schema.org/draft-07/schema#',
4012
- },
4013
- },
4014
- });
4015
- });
4016
-
4017
- it('should pass json schema response format as a tool', async () => {
4018
- const requestBody = await server.calls[0].requestBodyJson;
4019
-
4020
- expect(requestBody.toolConfig).toBeDefined();
4021
- expect(requestBody.toolConfig.tools).toHaveLength(1);
4022
- expect(requestBody.toolConfig.tools[0].toolSpec.name).toBe('json');
4023
- expect(requestBody.toolConfig.tools[0].toolSpec.description).toBe(
4024
- 'Respond with a JSON object.',
4025
- );
4026
- expect(requestBody.toolConfig.toolChoice).toEqual({ any: {} });
4027
- });
4028
-
4029
- it('should return the json response as text', async () => {
4030
- expect(result.content).toMatchInlineSnapshot(`
4031
- [
4032
- {
4033
- "text": "{"elements":[{"location":"San Francisco","temperature":-5,"condition":"snowy"},{"location":"London","temperature":0,"condition":"snowy"}]}",
4034
- "type": "text",
4035
- },
4036
- ]
4037
- `);
4038
- });
4039
-
4040
- it('should send stop finish reason when json tool is used', async () => {
4041
- expect(result.finishReason).toMatchInlineSnapshot(`
4042
- {
4043
- "raw": "tool_use",
4044
- "unified": "stop",
4045
- }
4046
- `);
4047
- });
4048
-
4049
- it('should set isJsonResponseFromTool in provider metadata', async () => {
4050
- expect(result.providerMetadata?.bedrock?.isJsonResponseFromTool).toBe(
4051
- true,
4052
- );
4053
- });
4054
- });
4055
-
4056
- describe('json schema response format with other tool response', () => {
4057
- let result: Awaited<ReturnType<typeof model.doGenerate>>;
4058
-
4059
- beforeEach(async () => {
4060
- prepareJsonFixtureResponse('bedrock-json-other-tool.1');
4061
-
4062
- result = await model.doGenerate({
4063
- prompt: [{ role: 'user', content: [{ type: 'text', text: 'Test' }] }],
4064
- tools: [
4065
- {
4066
- type: 'function',
4067
- name: 'get-weather',
4068
- description: 'Get the weather in a location',
4069
- inputSchema: {
4070
- type: 'object',
4071
- properties: {
4072
- location: { type: 'string' },
4073
- },
4074
- required: ['location'],
4075
- additionalProperties: false,
4076
- $schema: 'http://json-schema.org/draft-07/schema#',
4077
- },
4078
- },
4079
- ],
4080
- responseFormat: {
4081
- type: 'json',
4082
- schema: {
4083
- type: 'object',
4084
- properties: {
4085
- weather: { type: 'string' },
4086
- temperature: { type: 'number' },
4087
- },
4088
- required: ['weather', 'temperature'],
4089
- additionalProperties: false,
4090
- $schema: 'http://json-schema.org/draft-07/schema#',
4091
- },
4092
- },
4093
- });
4094
- });
4095
-
4096
- it('should pass the tool and the json schema response format as tools', async () => {
4097
- const requestBody = await server.calls[0].requestBodyJson;
4098
-
4099
- expect(requestBody.toolConfig).toBeDefined();
4100
- expect(requestBody.toolConfig.tools).toHaveLength(2);
4101
- expect(requestBody.toolConfig.tools[0].toolSpec.name).toBe('get-weather');
4102
- expect(requestBody.toolConfig.tools[1].toolSpec.name).toBe('json');
4103
- expect(requestBody.toolConfig.toolChoice).toEqual({ any: {} });
4104
- });
4105
-
4106
- it('should return the regular tool call', async () => {
4107
- expect(result.content).toMatchInlineSnapshot(`
4108
- [
4109
- {
4110
- "input": "{"location":"San Francisco"}",
4111
- "toolCallId": "toolu_01PQjhxo3eirCdKNvCJrKc8f",
4112
- "toolName": "get-weather",
4113
- "type": "tool-call",
4114
- },
4115
- ]
4116
- `);
4117
- });
4118
-
4119
- it('should send tool-calls finish reason', async () => {
4120
- expect(result.finishReason).toMatchInlineSnapshot(`
4121
- {
4122
- "raw": "tool_use",
4123
- "unified": "tool-calls",
4124
- }
4125
- `);
4126
- });
4127
- });
4128
-
4129
- it('should handle unsupported response format types', async () => {
4130
- prepareJsonResponse({});
4131
-
4132
- const result = await model.doGenerate({
4133
- prompt: TEST_PROMPT,
4134
- responseFormat: { type: 'xml' as any },
4135
- });
4136
-
4137
- expect(result.warnings).toMatchInlineSnapshot(`
4138
- [
4139
- {
4140
- "details": "Only text and json response formats are supported.",
4141
- "feature": "responseFormat",
4142
- "type": "unsupported",
4143
- },
4144
- ]
4145
- `);
4146
- });
4147
-
4148
- it('should omit toolConfig when conversation has tool calls but toolChoice is none', async () => {
4149
- prepareJsonResponse({});
4150
-
4151
- const conversationWithToolCalls: LanguageModelV3Prompt = [
4152
- {
4153
- role: 'user',
4154
- content: [{ type: 'text', text: 'What is the weather in Toronto?' }],
4155
- },
4156
- {
4157
- role: 'assistant',
4158
- content: [
4159
- {
4160
- type: 'tool-call',
4161
- toolCallId: 'tool-call-1',
4162
- toolName: 'weather',
4163
- input: { city: 'Toronto' },
4164
- },
4165
- ],
4166
- },
4167
- {
4168
- role: 'tool',
4169
- content: [
4170
- {
4171
- type: 'tool-result',
4172
- toolCallId: 'tool-call-1',
4173
- toolName: 'weather',
4174
- output: {
4175
- type: 'text',
4176
- value: 'The weather in Toronto is 20°C.',
4177
- },
4178
- },
4179
- ],
4180
- },
4181
- {
4182
- role: 'user',
4183
- content: [{ type: 'text', text: 'Now give me a summary.' }],
4184
- },
4185
- ];
4186
-
4187
- await model.doGenerate({
4188
- prompt: conversationWithToolCalls,
4189
- tools: [
4190
- {
4191
- type: 'function',
4192
- name: 'weather',
4193
- inputSchema: {
4194
- type: 'object',
4195
- properties: { city: { type: 'string' } },
4196
- required: ['city'],
4197
- additionalProperties: false,
4198
- $schema: 'http://json-schema.org/draft-07/schema#',
4199
- },
4200
- },
4201
- ],
4202
- toolChoice: { type: 'none' },
4203
- });
4204
-
4205
- const requestBody = await server.calls[0].requestBodyJson;
4206
-
4207
- expect(requestBody.toolConfig).toMatchInlineSnapshot(`undefined`);
4208
- });
4209
-
4210
- it('should clamp temperature above 1 to 1 and add warning', async () => {
4211
- prepareJsonResponse({});
4212
-
4213
- const result = await model.doGenerate({
4214
- prompt: TEST_PROMPT,
4215
- temperature: 1.5,
4216
- });
4217
-
4218
- const requestBody = await server.calls[0].requestBodyJson;
4219
-
4220
- expect(requestBody.inferenceConfig.temperature).toBe(1);
4221
- expect(result.warnings).toMatchInlineSnapshot(`
4222
- [
4223
- {
4224
- "details": "1.5 exceeds bedrock maximum of 1.0. clamped to 1.0",
4225
- "feature": "temperature",
4226
- "type": "unsupported",
4227
- },
4228
- ]
4229
- `);
4230
- });
4231
-
4232
- it('should clamp temperature below 0 to 0 and add warning', async () => {
4233
- prepareJsonResponse({});
4234
-
4235
- const result = await model.doGenerate({
4236
- prompt: TEST_PROMPT,
4237
- temperature: -0.5,
4238
- });
4239
-
4240
- const requestBody = await server.calls[0].requestBodyJson;
4241
-
4242
- expect(requestBody.inferenceConfig.temperature).toBe(0);
4243
- expect(result.warnings).toMatchInlineSnapshot(`
4244
- [
4245
- {
4246
- "details": "-0.5 is below bedrock minimum of 0. clamped to 0",
4247
- "feature": "temperature",
4248
- "type": "unsupported",
4249
- },
4250
- ]
4251
- `);
4252
- });
4253
-
4254
- it('should not clamp valid temperature between 0 and 1', async () => {
4255
- prepareJsonResponse({});
4256
-
4257
- const result = await model.doGenerate({
4258
- prompt: TEST_PROMPT,
4259
- temperature: 0.7,
4260
- });
4261
-
4262
- const requestBody = await server.calls[0].requestBodyJson;
4263
-
4264
- expect(requestBody.inferenceConfig.temperature).toBe(0.7);
4265
- expect(result.warnings).toMatchInlineSnapshot(`[]`);
4266
- });
4267
-
4268
- it('should include text content before JSON tool call in doGenerate', async () => {
4269
- prepareJsonFixtureResponse('bedrock-json-tool.2');
4270
-
4271
- const result = await model.doGenerate({
4272
- prompt: [
4273
- { role: 'user', content: [{ type: 'text', text: 'Generate JSON' }] },
4274
- ],
4275
- responseFormat: {
4276
- type: 'json',
4277
- schema: {
4278
- type: 'object',
4279
- properties: { value: { type: 'string' } },
4280
- required: ['value'],
4281
- },
4282
- },
4283
- });
4284
-
4285
- // Text before the JSON tool call should be included
4286
- expect(result.content).toMatchInlineSnapshot(`
4287
- [
4288
- {
4289
- "text": "Let me generate that JSON for you.",
4290
- "type": "text",
4291
- },
4292
- {
4293
- "text": "{"value":"test data"}",
4294
- "type": "text",
4295
- },
4296
- ]
4297
- `);
4298
-
4299
- expect(result.providerMetadata?.bedrock?.isJsonResponseFromTool).toBe(true);
4300
- expect(result.finishReason).toMatchInlineSnapshot(`
4301
- {
4302
- "raw": "tool_use",
4303
- "unified": "stop",
4304
- }
4305
- `);
4306
- });
4307
-
4308
- it('should preserve text response before JSON output (answering question then returning structured data)', async () => {
4309
- prepareJsonFixtureResponse('bedrock-json-tool-with-answer.1');
4310
-
4311
- const result = await model.doGenerate({
4312
- prompt: [
4313
- {
4314
- role: 'user',
4315
- content: [
4316
- {
4317
- type: 'text',
4318
- text: "What's 2+2? Also the name to use is John Doe",
4319
- },
4320
- ],
4321
- },
4322
- ],
4323
- responseFormat: {
4324
- type: 'json',
4325
- schema: {
4326
- type: 'object',
4327
- properties: {
4328
- firstName: { type: 'string' },
4329
- lastName: { type: 'string' },
4330
- },
4331
- required: ['firstName', 'lastName'],
4332
- additionalProperties: false,
4333
- $schema: 'http://json-schema.org/draft-07/schema#',
4334
- },
4335
- },
4336
- });
4337
-
4338
- // Should preserve the answer text AND the structured JSON output
4339
- expect(result.content).toMatchInlineSnapshot(`
4340
- [
4341
- {
4342
- "text": "2 + 2 equals 4.",
4343
- "type": "text",
4344
- },
4345
- {
4346
- "text": "{"firstName":"John","lastName":"Doe"}",
4347
- "type": "text",
4348
- },
4349
- ]
4350
- `);
4351
-
4352
- expect(result.finishReason).toMatchInlineSnapshot(`
4353
- {
4354
- "raw": "tool_use",
4355
- "unified": "stop",
4356
- }
4357
- `);
4358
- expect(result.providerMetadata?.bedrock?.isJsonResponseFromTool).toBe(true);
4359
- });
4360
-
4361
- it('should include multiple text blocks before JSON tool call in doGenerate', async () => {
4362
- prepareJsonFixtureResponse('bedrock-json-tool.3');
4363
-
4364
- const result = await model.doGenerate({
4365
- prompt: [
4366
- { role: 'user', content: [{ type: 'text', text: 'Generate data' }] },
4367
- ],
4368
- responseFormat: {
4369
- type: 'json',
4370
- schema: {
4371
- type: 'object',
4372
- properties: { result: { type: 'number' } },
4373
- required: ['result'],
4374
- },
4375
- },
4376
- });
4377
-
4378
- // All text blocks before the JSON tool call should be included
4379
- expect(result.content).toMatchInlineSnapshot(`
4380
- [
4381
- {
4382
- "text": "First text block.",
4383
- "type": "text",
4384
- },
4385
- {
4386
- "text": "Second text block.",
4387
- "type": "text",
4388
- },
4389
- {
4390
- "text": "Third text block before JSON.",
4391
- "type": "text",
4392
- },
4393
- {
4394
- "text": "{"result":42}",
4395
- "type": "text",
4396
- },
4397
- ]
4398
- `);
4399
-
4400
- expect(result.providerMetadata?.bedrock?.isJsonResponseFromTool).toBe(true);
4401
- });
4402
-
4403
- it('should handle regular tool call before JSON tool call in doGenerate', async () => {
4404
- prepareJsonFixtureResponse('bedrock-json-with-tool.1');
4405
-
4406
- const result = await model.doGenerate({
4407
- prompt: [{ role: 'user', content: [{ type: 'text', text: 'Test' }] }],
4408
- tools: [
4409
- {
4410
- type: 'function',
4411
- name: 'get-weather',
4412
- inputSchema: {
4413
- type: 'object',
4414
- properties: { location: { type: 'string' } },
4415
- required: ['location'],
4416
- },
4417
- },
4418
- ],
4419
- responseFormat: {
4420
- type: 'json',
4421
- schema: {
4422
- type: 'object',
4423
- properties: { result: { type: 'string' } },
4424
- required: ['result'],
4425
- },
4426
- },
4427
- });
4428
-
4429
- // Should include text, regular tool call, more text, and JSON as text
4430
- expect(result.content).toMatchInlineSnapshot(`
4431
- [
4432
- {
4433
- "text": "Let me call a tool first.",
4434
- "type": "text",
4435
- },
4436
- {
4437
- "input": "{"location":"SF"}",
4438
- "toolCallId": "tool-1",
4439
- "toolName": "get-weather",
4440
- "type": "tool-call",
4441
- },
4442
- {
4443
- "text": "Now the JSON.",
4444
- "type": "text",
4445
- },
4446
- {
4447
- "text": "{"result":"data"}",
4448
- "type": "text",
4449
- },
4450
- ]
4451
- `);
4452
-
4453
- expect(result.providerMetadata?.bedrock?.isJsonResponseFromTool).toBe(true);
4454
- expect(result.finishReason).toMatchInlineSnapshot(`
4455
- {
4456
- "raw": "tool_use",
4457
- "unified": "stop",
4458
- }
4459
- `);
4460
- });
4461
-
4462
- it('should handle multiple regular tool calls before JSON tool call in doGenerate', async () => {
4463
- prepareJsonFixtureResponse('bedrock-json-with-tools.1');
4464
-
4465
- const result = await model.doGenerate({
4466
- prompt: [{ role: 'user', content: [{ type: 'text', text: 'Test' }] }],
4467
- tools: [
4468
- {
4469
- type: 'function',
4470
- name: 'tool-a',
4471
- inputSchema: {
4472
- type: 'object',
4473
- properties: { param: { type: 'string' } },
4474
- },
4475
- },
4476
- {
4477
- type: 'function',
4478
- name: 'tool-b',
4479
- inputSchema: {
4480
- type: 'object',
4481
- properties: { param: { type: 'string' } },
4482
- },
4483
- },
4484
- ],
4485
- responseFormat: {
4486
- type: 'json',
4487
- schema: {
4488
- type: 'object',
4489
- properties: { final: { type: 'string' } },
4490
- required: ['final'],
4491
- },
4492
- },
4493
- });
4494
-
4495
- // Should preserve all content
4496
- expect(result.content).toMatchInlineSnapshot(`
4497
- [
4498
- {
4499
- "text": "Calling multiple tools.",
4500
- "type": "text",
4501
- },
4502
- {
4503
- "input": "{"param":"a"}",
4504
- "toolCallId": "tool-1",
4505
- "toolName": "tool-a",
4506
- "type": "tool-call",
4507
- },
4508
- {
4509
- "input": "{"param":"b"}",
4510
- "toolCallId": "tool-2",
4511
- "toolName": "tool-b",
4512
- "type": "tool-call",
4513
- },
4514
- {
4515
- "text": "Finally the JSON response.",
4516
- "type": "text",
4517
- },
4518
- {
4519
- "text": "{"final":"result"}",
4520
- "type": "text",
4521
- },
4522
- ]
4523
- `);
4524
-
4525
- expect(result.providerMetadata?.bedrock?.isJsonResponseFromTool).toBe(true);
4526
- expect(result.finishReason).toMatchInlineSnapshot(`
4527
- {
4528
- "raw": "tool_use",
4529
- "unified": "stop",
4530
- }
4531
- `);
4532
- });
4533
-
4534
- it('should support tool calls with empty input (no arguments)', async () => {
4535
- prepareJsonFixtureResponse('bedrock-tool-no-args');
4536
-
4537
- const result = await model.doGenerate({
4538
- tools: [
4539
- {
4540
- type: 'function',
4541
- name: 'updateIssueList',
4542
- inputSchema: {
4543
- type: 'object',
4544
- properties: {},
4545
- required: [],
4546
- additionalProperties: false,
4547
- $schema: 'http://json-schema.org/draft-07/schema#',
4548
- },
4549
- },
4550
- ],
4551
- prompt: TEST_PROMPT,
4552
- });
4553
-
4554
- expect(result.content).toMatchInlineSnapshot(`
4555
- [
4556
- {
4557
- "text": "I'll update the issue list for you.",
4558
- "type": "text",
4559
- },
4560
- {
4561
- "input": "{}",
4562
- "toolCallId": "tool-use-id",
4563
- "toolName": "updateIssueList",
4564
- "type": "tool-call",
4565
- },
4566
- ]
4567
- `);
4568
- });
4569
- });