@ai-sdk/amazon-bedrock 4.0.23 → 4.0.25

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 (68) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/anthropic/index.js +1 -1
  3. package/dist/anthropic/index.mjs +1 -1
  4. package/dist/index.js +33 -8
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +33 -8
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +4 -3
  9. package/src/__fixtures__/bedrock-json-only-text-first.1.chunks.txt +7 -0
  10. package/src/__fixtures__/bedrock-json-other-tool.1.chunks.txt +6 -0
  11. package/src/__fixtures__/bedrock-json-other-tool.1.json +24 -0
  12. package/src/__fixtures__/bedrock-json-tool-text-then-weather-then-json.1.chunks.txt +12 -0
  13. package/src/__fixtures__/bedrock-json-tool-with-answer.1.json +29 -0
  14. package/src/__fixtures__/bedrock-json-tool.1.chunks.txt +4 -0
  15. package/src/__fixtures__/bedrock-json-tool.1.json +35 -0
  16. package/src/__fixtures__/bedrock-json-tool.2.chunks.txt +6 -0
  17. package/src/__fixtures__/bedrock-json-tool.2.json +28 -0
  18. package/src/__fixtures__/bedrock-json-tool.3.chunks.txt +7 -0
  19. package/src/__fixtures__/bedrock-json-tool.3.json +36 -0
  20. package/src/__fixtures__/bedrock-json-with-tool.1.chunks.txt +9 -0
  21. package/src/__fixtures__/bedrock-json-with-tool.1.json +41 -0
  22. package/src/__fixtures__/bedrock-json-with-tools.1.chunks.txt +12 -0
  23. package/src/__fixtures__/bedrock-json-with-tools.1.json +50 -0
  24. package/src/__fixtures__/bedrock-tool-call.1.chunks.txt +6 -0
  25. package/src/__fixtures__/bedrock-tool-call.1.json +24 -0
  26. package/src/__fixtures__/bedrock-tool-no-args.chunks.txt +8 -0
  27. package/src/__fixtures__/bedrock-tool-no-args.json +25 -0
  28. package/src/anthropic/bedrock-anthropic-fetch.test.ts +344 -0
  29. package/src/anthropic/bedrock-anthropic-fetch.ts +62 -0
  30. package/src/anthropic/bedrock-anthropic-options.ts +28 -0
  31. package/src/anthropic/bedrock-anthropic-provider.test.ts +456 -0
  32. package/src/anthropic/bedrock-anthropic-provider.ts +357 -0
  33. package/src/anthropic/index.ts +9 -0
  34. package/src/bedrock-api-types.ts +195 -0
  35. package/src/bedrock-chat-language-model.test.ts +4569 -0
  36. package/src/bedrock-chat-language-model.ts +1019 -0
  37. package/src/bedrock-chat-options.ts +114 -0
  38. package/src/bedrock-embedding-model.test.ts +148 -0
  39. package/src/bedrock-embedding-model.ts +104 -0
  40. package/src/bedrock-embedding-options.ts +24 -0
  41. package/src/bedrock-error.ts +6 -0
  42. package/src/bedrock-event-stream-decoder.ts +59 -0
  43. package/src/bedrock-event-stream-response-handler.test.ts +233 -0
  44. package/src/bedrock-event-stream-response-handler.ts +57 -0
  45. package/src/bedrock-image-model.test.ts +866 -0
  46. package/src/bedrock-image-model.ts +297 -0
  47. package/src/bedrock-image-settings.ts +6 -0
  48. package/src/bedrock-prepare-tools.ts +190 -0
  49. package/src/bedrock-provider.test.ts +457 -0
  50. package/src/bedrock-provider.ts +351 -0
  51. package/src/bedrock-sigv4-fetch.test.ts +675 -0
  52. package/src/bedrock-sigv4-fetch.ts +138 -0
  53. package/src/convert-bedrock-usage.test.ts +207 -0
  54. package/src/convert-bedrock-usage.ts +50 -0
  55. package/src/convert-to-bedrock-chat-messages.test.ts +1175 -0
  56. package/src/convert-to-bedrock-chat-messages.ts +452 -0
  57. package/src/index.ts +10 -0
  58. package/src/inject-fetch-headers.test.ts +135 -0
  59. package/src/inject-fetch-headers.ts +32 -0
  60. package/src/map-bedrock-finish-reason.ts +22 -0
  61. package/src/normalize-tool-call-id.test.ts +72 -0
  62. package/src/normalize-tool-call-id.ts +36 -0
  63. package/src/reranking/__fixtures__/bedrock-reranking.1.json +12 -0
  64. package/src/reranking/bedrock-reranking-api.ts +44 -0
  65. package/src/reranking/bedrock-reranking-model.test.ts +299 -0
  66. package/src/reranking/bedrock-reranking-model.ts +115 -0
  67. package/src/reranking/bedrock-reranking-options.ts +36 -0
  68. package/src/version.ts +6 -0
@@ -0,0 +1,344 @@
1
+ import { createBedrockAnthropicFetch } from './bedrock-anthropic-fetch';
2
+ import { EventStreamCodec } from '@smithy/eventstream-codec';
3
+ import { toUtf8, fromUtf8 } from '@smithy/util-utf8';
4
+ import { describe, it, expect, vi } from 'vitest';
5
+
6
+ describe('createBedrockAnthropicFetch', () => {
7
+ function createMockResponse(
8
+ body: ReadableStream<Uint8Array> | null,
9
+ contentType: string,
10
+ ): Response {
11
+ return new Response(body, {
12
+ headers: { 'content-type': contentType },
13
+ });
14
+ }
15
+
16
+ function createMockFetch(
17
+ response: Response,
18
+ ): (url: RequestInfo | URL, options?: RequestInit) => Promise<Response> {
19
+ return vi.fn().mockResolvedValue(response);
20
+ }
21
+
22
+ it('should pass through non-streaming responses unchanged', async () => {
23
+ const jsonBody = JSON.stringify({ id: 'msg_123', content: [] });
24
+ const mockResponse = new Response(jsonBody, {
25
+ headers: { 'content-type': 'application/json' },
26
+ });
27
+ const baseFetch = createMockFetch(mockResponse);
28
+ const wrappedFetch = createBedrockAnthropicFetch(baseFetch);
29
+
30
+ const response = await wrappedFetch('https://example.com', {});
31
+
32
+ expect(response.headers.get('content-type')).toBe('application/json');
33
+ expect(await response.text()).toBe(jsonBody);
34
+ });
35
+
36
+ it('should transform Bedrock event stream to SSE format', async () => {
37
+ const codec = new EventStreamCodec(toUtf8, fromUtf8);
38
+
39
+ // Create a mock Anthropic event
40
+ const anthropicEvent = JSON.stringify({
41
+ type: 'content_block_delta',
42
+ delta: { type: 'text_delta', text: 'Hello' },
43
+ });
44
+
45
+ // Encode as Bedrock chunk with base64 bytes
46
+ const chunkPayload = JSON.stringify({
47
+ bytes: btoa(anthropicEvent),
48
+ });
49
+
50
+ // Create Bedrock event stream message
51
+ const bedrockEvent = codec.encode({
52
+ headers: {
53
+ ':message-type': { type: 'string', value: 'event' },
54
+ ':event-type': { type: 'string', value: 'chunk' },
55
+ },
56
+ body: fromUtf8(chunkPayload),
57
+ });
58
+
59
+ // Create a readable stream from the event
60
+ const stream = new ReadableStream({
61
+ start(controller) {
62
+ controller.enqueue(bedrockEvent);
63
+ controller.close();
64
+ },
65
+ });
66
+
67
+ const mockResponse = createMockResponse(
68
+ stream,
69
+ 'application/vnd.amazon.eventstream',
70
+ );
71
+ const baseFetch = createMockFetch(mockResponse);
72
+ const wrappedFetch = createBedrockAnthropicFetch(baseFetch);
73
+
74
+ const response = await wrappedFetch('https://example.com', {});
75
+
76
+ expect(response.headers.get('content-type')).toBe('text/event-stream');
77
+
78
+ const reader = response.body!.getReader();
79
+ const { value } = await reader.read();
80
+ const text = new TextDecoder().decode(value);
81
+
82
+ expect(text).toBe(`data: ${anthropicEvent}\n\n`);
83
+ });
84
+
85
+ it('should handle messageStop event and emit [DONE]', async () => {
86
+ const codec = new EventStreamCodec(toUtf8, fromUtf8);
87
+
88
+ const bedrockEvent = codec.encode({
89
+ headers: {
90
+ ':message-type': { type: 'string', value: 'event' },
91
+ ':event-type': { type: 'string', value: 'messageStop' },
92
+ },
93
+ body: fromUtf8('{}'),
94
+ });
95
+
96
+ const stream = new ReadableStream({
97
+ start(controller) {
98
+ controller.enqueue(bedrockEvent);
99
+ controller.close();
100
+ },
101
+ });
102
+
103
+ const mockResponse = createMockResponse(
104
+ stream,
105
+ 'application/vnd.amazon.eventstream',
106
+ );
107
+ const baseFetch = createMockFetch(mockResponse);
108
+ const wrappedFetch = createBedrockAnthropicFetch(baseFetch);
109
+
110
+ const response = await wrappedFetch('https://example.com', {});
111
+ const reader = response.body!.getReader();
112
+ const { value } = await reader.read();
113
+ const text = new TextDecoder().decode(value);
114
+
115
+ expect(text).toBe('data: [DONE]\n\n');
116
+ });
117
+
118
+ it('should handle exception messages', async () => {
119
+ const codec = new EventStreamCodec(toUtf8, fromUtf8);
120
+
121
+ const errorData = JSON.stringify({ message: 'Rate limit exceeded' });
122
+ const bedrockEvent = codec.encode({
123
+ headers: {
124
+ ':message-type': { type: 'string', value: 'exception' },
125
+ ':exception-type': { type: 'string', value: 'ThrottlingException' },
126
+ },
127
+ body: fromUtf8(errorData),
128
+ });
129
+
130
+ const stream = new ReadableStream({
131
+ start(controller) {
132
+ controller.enqueue(bedrockEvent);
133
+ controller.close();
134
+ },
135
+ });
136
+
137
+ const mockResponse = createMockResponse(
138
+ stream,
139
+ 'application/vnd.amazon.eventstream',
140
+ );
141
+ const baseFetch = createMockFetch(mockResponse);
142
+ const wrappedFetch = createBedrockAnthropicFetch(baseFetch);
143
+
144
+ const response = await wrappedFetch('https://example.com', {});
145
+ const reader = response.body!.getReader();
146
+ const { value } = await reader.read();
147
+ const text = new TextDecoder().decode(value);
148
+
149
+ expect(text).toBe(
150
+ `data: ${JSON.stringify({ type: 'error', error: errorData })}\n\n`,
151
+ );
152
+ });
153
+
154
+ it('should handle multiple events in sequence', async () => {
155
+ const codec = new EventStreamCodec(toUtf8, fromUtf8);
156
+
157
+ const event1 = JSON.stringify({
158
+ type: 'message_start',
159
+ message: { id: 'msg_123' },
160
+ });
161
+ const event2 = JSON.stringify({
162
+ type: 'content_block_delta',
163
+ delta: { text: 'Hi' },
164
+ });
165
+
166
+ const chunk1 = codec.encode({
167
+ headers: {
168
+ ':message-type': { type: 'string', value: 'event' },
169
+ ':event-type': { type: 'string', value: 'chunk' },
170
+ },
171
+ body: fromUtf8(JSON.stringify({ bytes: btoa(event1) })),
172
+ });
173
+
174
+ const chunk2 = codec.encode({
175
+ headers: {
176
+ ':message-type': { type: 'string', value: 'event' },
177
+ ':event-type': { type: 'string', value: 'chunk' },
178
+ },
179
+ body: fromUtf8(JSON.stringify({ bytes: btoa(event2) })),
180
+ });
181
+
182
+ const stopEvent = codec.encode({
183
+ headers: {
184
+ ':message-type': { type: 'string', value: 'event' },
185
+ ':event-type': { type: 'string', value: 'messageStop' },
186
+ },
187
+ body: fromUtf8('{}'),
188
+ });
189
+
190
+ const stream = new ReadableStream({
191
+ start(controller) {
192
+ controller.enqueue(chunk1);
193
+ controller.enqueue(chunk2);
194
+ controller.enqueue(stopEvent);
195
+ controller.close();
196
+ },
197
+ });
198
+
199
+ const mockResponse = createMockResponse(
200
+ stream,
201
+ 'application/vnd.amazon.eventstream',
202
+ );
203
+ const baseFetch = createMockFetch(mockResponse);
204
+ const wrappedFetch = createBedrockAnthropicFetch(baseFetch);
205
+
206
+ const response = await wrappedFetch('https://example.com', {});
207
+ const reader = response.body!.getReader();
208
+
209
+ const chunks: string[] = [];
210
+ while (true) {
211
+ const { done, value } = await reader.read();
212
+ if (done) break;
213
+ chunks.push(new TextDecoder().decode(value));
214
+ }
215
+
216
+ const fullText = chunks.join('');
217
+ expect(fullText).toContain(`data: ${event1}\n\n`);
218
+ expect(fullText).toContain(`data: ${event2}\n\n`);
219
+ expect(fullText).toContain('data: [DONE]\n\n');
220
+ });
221
+
222
+ it('should handle chunked event data spanning multiple network chunks', async () => {
223
+ const codec = new EventStreamCodec(toUtf8, fromUtf8);
224
+
225
+ const anthropicEvent = JSON.stringify({
226
+ type: 'content_block_delta',
227
+ delta: { text: 'Hello World' },
228
+ });
229
+
230
+ const bedrockEvent = codec.encode({
231
+ headers: {
232
+ ':message-type': { type: 'string', value: 'event' },
233
+ ':event-type': { type: 'string', value: 'chunk' },
234
+ },
235
+ body: fromUtf8(JSON.stringify({ bytes: btoa(anthropicEvent) })),
236
+ });
237
+
238
+ // Split the event into two chunks to simulate network fragmentation
239
+ const midpoint = Math.floor(bedrockEvent.length / 2);
240
+ const firstHalf = bedrockEvent.slice(0, midpoint);
241
+ const secondHalf = bedrockEvent.slice(midpoint);
242
+
243
+ const stream = new ReadableStream({
244
+ start(controller) {
245
+ controller.enqueue(firstHalf);
246
+ controller.enqueue(secondHalf);
247
+ controller.close();
248
+ },
249
+ });
250
+
251
+ const mockResponse = createMockResponse(
252
+ stream,
253
+ 'application/vnd.amazon.eventstream',
254
+ );
255
+ const baseFetch = createMockFetch(mockResponse);
256
+ const wrappedFetch = createBedrockAnthropicFetch(baseFetch);
257
+
258
+ const response = await wrappedFetch('https://example.com', {});
259
+ const reader = response.body!.getReader();
260
+
261
+ const chunks: string[] = [];
262
+ while (true) {
263
+ const { done, value } = await reader.read();
264
+ if (done) break;
265
+ if (value) {
266
+ chunks.push(new TextDecoder().decode(value));
267
+ }
268
+ }
269
+
270
+ const fullText = chunks.join('');
271
+ expect(fullText).toBe(`data: ${anthropicEvent}\n\n`);
272
+ });
273
+
274
+ it('should preserve response status and statusText', async () => {
275
+ const codec = new EventStreamCodec(toUtf8, fromUtf8);
276
+
277
+ const bedrockEvent = codec.encode({
278
+ headers: {
279
+ ':message-type': { type: 'string', value: 'event' },
280
+ ':event-type': { type: 'string', value: 'messageStop' },
281
+ },
282
+ body: fromUtf8('{}'),
283
+ });
284
+
285
+ const stream = new ReadableStream({
286
+ start(controller) {
287
+ controller.enqueue(bedrockEvent);
288
+ controller.close();
289
+ },
290
+ });
291
+
292
+ const mockResponse = new Response(stream, {
293
+ status: 200,
294
+ statusText: 'OK',
295
+ headers: { 'content-type': 'application/vnd.amazon.eventstream' },
296
+ });
297
+ const baseFetch = createMockFetch(mockResponse);
298
+ const wrappedFetch = createBedrockAnthropicFetch(baseFetch);
299
+
300
+ const response = await wrappedFetch('https://example.com', {});
301
+
302
+ expect(response.status).toBe(200);
303
+ expect(response.statusText).toBe('OK');
304
+ });
305
+
306
+ it('should handle chunk events with missing bytes field', async () => {
307
+ const codec = new EventStreamCodec(toUtf8, fromUtf8);
308
+
309
+ // Create a chunk without a bytes field - this should fall back to emitting raw data
310
+ const chunkPayload = JSON.stringify({
311
+ someOtherField: 'value',
312
+ });
313
+
314
+ const bedrockEvent = codec.encode({
315
+ headers: {
316
+ ':message-type': { type: 'string', value: 'event' },
317
+ ':event-type': { type: 'string', value: 'chunk' },
318
+ },
319
+ body: fromUtf8(chunkPayload),
320
+ });
321
+
322
+ const stream = new ReadableStream({
323
+ start(controller) {
324
+ controller.enqueue(bedrockEvent);
325
+ controller.close();
326
+ },
327
+ });
328
+
329
+ const mockResponse = createMockResponse(
330
+ stream,
331
+ 'application/vnd.amazon.eventstream',
332
+ );
333
+ const baseFetch = createMockFetch(mockResponse);
334
+ const wrappedFetch = createBedrockAnthropicFetch(baseFetch);
335
+
336
+ const response = await wrappedFetch('https://example.com', {});
337
+ const reader = response.body!.getReader();
338
+ const { value } = await reader.read();
339
+ const text = new TextDecoder().decode(value);
340
+
341
+ // Should emit the raw payload data as fallback
342
+ expect(text).toBe(`data: ${chunkPayload}\n\n`);
343
+ });
344
+ });
@@ -0,0 +1,62 @@
1
+ import { FetchFunction, safeParseJSON } from '@ai-sdk/provider-utils';
2
+ import { createBedrockEventStreamDecoder } from '../bedrock-event-stream-decoder';
3
+
4
+ export function createBedrockAnthropicFetch(
5
+ baseFetch: FetchFunction,
6
+ ): FetchFunction {
7
+ return async (url, options) => {
8
+ const response = await baseFetch(url, options);
9
+
10
+ const contentType = response.headers.get('content-type');
11
+ if (
12
+ contentType?.includes('application/vnd.amazon.eventstream') &&
13
+ response.body != null
14
+ ) {
15
+ const transformedBody = transformBedrockEventStreamToSSE(response.body);
16
+
17
+ return new Response(transformedBody, {
18
+ status: response.status,
19
+ statusText: response.statusText,
20
+ headers: new Headers({
21
+ ...Object.fromEntries(response.headers.entries()),
22
+ 'content-type': 'text/event-stream',
23
+ }),
24
+ });
25
+ }
26
+
27
+ return response;
28
+ };
29
+ }
30
+
31
+ function transformBedrockEventStreamToSSE(
32
+ body: ReadableStream<Uint8Array>,
33
+ ): ReadableStream<Uint8Array> {
34
+ const textEncoder = new TextEncoder();
35
+
36
+ return createBedrockEventStreamDecoder(body, async (event, controller) => {
37
+ if (event.messageType === 'event') {
38
+ if (event.eventType === 'chunk') {
39
+ const parsed = await safeParseJSON({ text: event.data });
40
+ if (!parsed.success) {
41
+ controller.enqueue(textEncoder.encode(`data: ${event.data}\n\n`));
42
+ return;
43
+ }
44
+ const bytes = (parsed.value as { bytes?: string }).bytes;
45
+ if (bytes) {
46
+ const anthropicEvent = atob(bytes);
47
+ controller.enqueue(textEncoder.encode(`data: ${anthropicEvent}\n\n`));
48
+ } else {
49
+ controller.enqueue(textEncoder.encode(`data: ${event.data}\n\n`));
50
+ }
51
+ } else if (event.eventType === 'messageStop') {
52
+ controller.enqueue(textEncoder.encode('data: [DONE]\n\n'));
53
+ }
54
+ } else if (event.messageType === 'exception') {
55
+ controller.enqueue(
56
+ textEncoder.encode(
57
+ `data: ${JSON.stringify({ type: 'error', error: event.data })}\n\n`,
58
+ ),
59
+ );
60
+ }
61
+ });
62
+ }
@@ -0,0 +1,28 @@
1
+ export type BedrockAnthropicModelId =
2
+ | 'anthropic.claude-opus-4-5-20251101-v1:0'
3
+ | 'anthropic.claude-sonnet-4-5-20250929-v1:0'
4
+ | 'anthropic.claude-opus-4-20250514-v1:0'
5
+ | 'anthropic.claude-sonnet-4-20250514-v1:0'
6
+ | 'anthropic.claude-opus-4-1-20250805-v1:0'
7
+ | 'anthropic.claude-haiku-4-5-20251001-v1:0'
8
+ | 'anthropic.claude-3-7-sonnet-20250219-v1:0'
9
+ | 'anthropic.claude-3-5-sonnet-20241022-v2:0'
10
+ | 'anthropic.claude-3-5-sonnet-20240620-v1:0'
11
+ | 'anthropic.claude-3-5-haiku-20241022-v1:0'
12
+ | 'anthropic.claude-3-opus-20240229-v1:0'
13
+ | 'anthropic.claude-3-sonnet-20240229-v1:0'
14
+ | 'anthropic.claude-3-haiku-20240307-v1:0'
15
+ | 'us.anthropic.claude-opus-4-5-20251101-v1:0'
16
+ | 'us.anthropic.claude-sonnet-4-5-20250929-v1:0'
17
+ | 'us.anthropic.claude-opus-4-20250514-v1:0'
18
+ | 'us.anthropic.claude-sonnet-4-20250514-v1:0'
19
+ | 'us.anthropic.claude-opus-4-1-20250805-v1:0'
20
+ | 'us.anthropic.claude-haiku-4-5-20251001-v1:0'
21
+ | 'us.anthropic.claude-3-7-sonnet-20250219-v1:0'
22
+ | 'us.anthropic.claude-3-5-sonnet-20241022-v2:0'
23
+ | 'us.anthropic.claude-3-5-sonnet-20240620-v1:0'
24
+ | 'us.anthropic.claude-3-5-haiku-20241022-v1:0'
25
+ | 'us.anthropic.claude-3-opus-20240229-v1:0'
26
+ | 'us.anthropic.claude-3-sonnet-20240229-v1:0'
27
+ | 'us.anthropic.claude-3-haiku-20240307-v1:0'
28
+ | (string & {});