@ai-sdk/amazon-bedrock 4.0.24 → 4.0.26

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 (67) 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 +1 -1
  5. package/dist/index.mjs +1 -1
  6. package/docs/08-amazon-bedrock.mdx +1453 -0
  7. package/package.json +11 -6
  8. package/src/__fixtures__/bedrock-json-only-text-first.1.chunks.txt +7 -0
  9. package/src/__fixtures__/bedrock-json-other-tool.1.chunks.txt +6 -0
  10. package/src/__fixtures__/bedrock-json-other-tool.1.json +24 -0
  11. package/src/__fixtures__/bedrock-json-tool-text-then-weather-then-json.1.chunks.txt +12 -0
  12. package/src/__fixtures__/bedrock-json-tool-with-answer.1.json +29 -0
  13. package/src/__fixtures__/bedrock-json-tool.1.chunks.txt +4 -0
  14. package/src/__fixtures__/bedrock-json-tool.1.json +35 -0
  15. package/src/__fixtures__/bedrock-json-tool.2.chunks.txt +6 -0
  16. package/src/__fixtures__/bedrock-json-tool.2.json +28 -0
  17. package/src/__fixtures__/bedrock-json-tool.3.chunks.txt +7 -0
  18. package/src/__fixtures__/bedrock-json-tool.3.json +36 -0
  19. package/src/__fixtures__/bedrock-json-with-tool.1.chunks.txt +9 -0
  20. package/src/__fixtures__/bedrock-json-with-tool.1.json +41 -0
  21. package/src/__fixtures__/bedrock-json-with-tools.1.chunks.txt +12 -0
  22. package/src/__fixtures__/bedrock-json-with-tools.1.json +50 -0
  23. package/src/__fixtures__/bedrock-tool-call.1.chunks.txt +6 -0
  24. package/src/__fixtures__/bedrock-tool-call.1.json +24 -0
  25. package/src/__fixtures__/bedrock-tool-no-args.chunks.txt +8 -0
  26. package/src/__fixtures__/bedrock-tool-no-args.json +25 -0
  27. package/src/anthropic/bedrock-anthropic-fetch.test.ts +344 -0
  28. package/src/anthropic/bedrock-anthropic-fetch.ts +62 -0
  29. package/src/anthropic/bedrock-anthropic-options.ts +28 -0
  30. package/src/anthropic/bedrock-anthropic-provider.test.ts +456 -0
  31. package/src/anthropic/bedrock-anthropic-provider.ts +357 -0
  32. package/src/anthropic/index.ts +9 -0
  33. package/src/bedrock-api-types.ts +195 -0
  34. package/src/bedrock-chat-language-model.test.ts +4569 -0
  35. package/src/bedrock-chat-language-model.ts +1019 -0
  36. package/src/bedrock-chat-options.ts +114 -0
  37. package/src/bedrock-embedding-model.test.ts +148 -0
  38. package/src/bedrock-embedding-model.ts +104 -0
  39. package/src/bedrock-embedding-options.ts +24 -0
  40. package/src/bedrock-error.ts +6 -0
  41. package/src/bedrock-event-stream-decoder.ts +59 -0
  42. package/src/bedrock-event-stream-response-handler.test.ts +233 -0
  43. package/src/bedrock-event-stream-response-handler.ts +57 -0
  44. package/src/bedrock-image-model.test.ts +866 -0
  45. package/src/bedrock-image-model.ts +297 -0
  46. package/src/bedrock-image-settings.ts +6 -0
  47. package/src/bedrock-prepare-tools.ts +190 -0
  48. package/src/bedrock-provider.test.ts +457 -0
  49. package/src/bedrock-provider.ts +351 -0
  50. package/src/bedrock-sigv4-fetch.test.ts +675 -0
  51. package/src/bedrock-sigv4-fetch.ts +138 -0
  52. package/src/convert-bedrock-usage.test.ts +207 -0
  53. package/src/convert-bedrock-usage.ts +50 -0
  54. package/src/convert-to-bedrock-chat-messages.test.ts +1175 -0
  55. package/src/convert-to-bedrock-chat-messages.ts +452 -0
  56. package/src/index.ts +10 -0
  57. package/src/inject-fetch-headers.test.ts +135 -0
  58. package/src/inject-fetch-headers.ts +32 -0
  59. package/src/map-bedrock-finish-reason.ts +22 -0
  60. package/src/normalize-tool-call-id.test.ts +72 -0
  61. package/src/normalize-tool-call-id.ts +36 -0
  62. package/src/reranking/__fixtures__/bedrock-reranking.1.json +12 -0
  63. package/src/reranking/bedrock-reranking-api.ts +44 -0
  64. package/src/reranking/bedrock-reranking-model.test.ts +299 -0
  65. package/src/reranking/bedrock-reranking-model.ts +115 -0
  66. package/src/reranking/bedrock-reranking-options.ts +36 -0
  67. package/src/version.ts +6 -0
@@ -0,0 +1,1019 @@
1
+ import {
2
+ JSONObject,
3
+ LanguageModelV3,
4
+ LanguageModelV3CallOptions,
5
+ LanguageModelV3Content,
6
+ LanguageModelV3FinishReason,
7
+ LanguageModelV3FunctionTool,
8
+ LanguageModelV3GenerateResult,
9
+ LanguageModelV3Reasoning,
10
+ LanguageModelV3StreamPart,
11
+ LanguageModelV3StreamResult,
12
+ SharedV3ProviderMetadata,
13
+ SharedV3Warning,
14
+ } from '@ai-sdk/provider';
15
+ import {
16
+ FetchFunction,
17
+ ParseResult,
18
+ Resolvable,
19
+ combineHeaders,
20
+ createJsonErrorResponseHandler,
21
+ createJsonResponseHandler,
22
+ parseProviderOptions,
23
+ postJsonToApi,
24
+ resolve,
25
+ } from '@ai-sdk/provider-utils';
26
+ import { z } from 'zod/v4';
27
+ import {
28
+ BEDROCK_STOP_REASONS,
29
+ BedrockConverseInput,
30
+ BedrockStopReason,
31
+ } from './bedrock-api-types';
32
+ import {
33
+ BedrockChatModelId,
34
+ bedrockProviderOptions,
35
+ } from './bedrock-chat-options';
36
+ import { BedrockErrorSchema } from './bedrock-error';
37
+ import { createBedrockEventStreamResponseHandler } from './bedrock-event-stream-response-handler';
38
+ import { prepareTools } from './bedrock-prepare-tools';
39
+ import { BedrockUsage, convertBedrockUsage } from './convert-bedrock-usage';
40
+ import { convertToBedrockChatMessages } from './convert-to-bedrock-chat-messages';
41
+ import { mapBedrockFinishReason } from './map-bedrock-finish-reason';
42
+ import { isMistralModel, normalizeToolCallId } from './normalize-tool-call-id';
43
+
44
+ type BedrockChatConfig = {
45
+ baseUrl: () => string;
46
+ headers: Resolvable<Record<string, string | undefined>>;
47
+ fetch?: FetchFunction;
48
+ generateId: () => string;
49
+ };
50
+
51
+ export class BedrockChatLanguageModel implements LanguageModelV3 {
52
+ readonly specificationVersion = 'v3';
53
+ readonly provider = 'amazon-bedrock';
54
+
55
+ constructor(
56
+ readonly modelId: BedrockChatModelId,
57
+ private readonly config: BedrockChatConfig,
58
+ ) {}
59
+
60
+ private async getArgs({
61
+ prompt,
62
+ maxOutputTokens,
63
+ temperature,
64
+ topP,
65
+ topK,
66
+ frequencyPenalty,
67
+ presencePenalty,
68
+ stopSequences,
69
+ responseFormat,
70
+ seed,
71
+ tools,
72
+ toolChoice,
73
+ providerOptions,
74
+ }: LanguageModelV3CallOptions): Promise<{
75
+ command: BedrockConverseInput;
76
+ warnings: SharedV3Warning[];
77
+ usesJsonResponseTool: boolean;
78
+ betas: Set<string>;
79
+ }> {
80
+ // Parse provider options
81
+ const bedrockOptions =
82
+ (await parseProviderOptions({
83
+ provider: 'bedrock',
84
+ providerOptions,
85
+ schema: bedrockProviderOptions,
86
+ })) ?? {};
87
+
88
+ const warnings: SharedV3Warning[] = [];
89
+
90
+ if (frequencyPenalty != null) {
91
+ warnings.push({
92
+ type: 'unsupported',
93
+ feature: 'frequencyPenalty',
94
+ });
95
+ }
96
+
97
+ if (presencePenalty != null) {
98
+ warnings.push({
99
+ type: 'unsupported',
100
+ feature: 'presencePenalty',
101
+ });
102
+ }
103
+
104
+ if (seed != null) {
105
+ warnings.push({
106
+ type: 'unsupported',
107
+ feature: 'seed',
108
+ });
109
+ }
110
+
111
+ if (temperature != null && temperature > 1) {
112
+ warnings.push({
113
+ type: 'unsupported',
114
+ feature: 'temperature',
115
+ details: `${temperature} exceeds bedrock maximum of 1.0. clamped to 1.0`,
116
+ });
117
+ temperature = 1;
118
+ } else if (temperature != null && temperature < 0) {
119
+ warnings.push({
120
+ type: 'unsupported',
121
+ feature: 'temperature',
122
+ details: `${temperature} is below bedrock minimum of 0. clamped to 0`,
123
+ });
124
+ temperature = 0;
125
+ }
126
+
127
+ if (
128
+ responseFormat != null &&
129
+ responseFormat.type !== 'text' &&
130
+ responseFormat.type !== 'json'
131
+ ) {
132
+ warnings.push({
133
+ type: 'unsupported',
134
+ feature: 'responseFormat',
135
+ details: 'Only text and json response formats are supported.',
136
+ });
137
+ }
138
+
139
+ const jsonResponseTool: LanguageModelV3FunctionTool | undefined =
140
+ responseFormat?.type === 'json' && responseFormat.schema != null
141
+ ? {
142
+ type: 'function',
143
+ name: 'json',
144
+ description: 'Respond with a JSON object.',
145
+ inputSchema: responseFormat.schema,
146
+ }
147
+ : undefined;
148
+
149
+ const { toolConfig, additionalTools, toolWarnings, betas } =
150
+ await prepareTools({
151
+ tools: jsonResponseTool ? [...(tools ?? []), jsonResponseTool] : tools,
152
+ toolChoice:
153
+ jsonResponseTool != null ? { type: 'required' } : toolChoice,
154
+ modelId: this.modelId,
155
+ });
156
+
157
+ warnings.push(...toolWarnings);
158
+
159
+ if (additionalTools) {
160
+ bedrockOptions.additionalModelRequestFields = {
161
+ ...bedrockOptions.additionalModelRequestFields,
162
+ ...additionalTools,
163
+ };
164
+ }
165
+
166
+ if (betas.size > 0 || bedrockOptions.anthropicBeta) {
167
+ const existingBetas = bedrockOptions.anthropicBeta ?? [];
168
+ const mergedBetas =
169
+ betas.size > 0
170
+ ? [...existingBetas, ...Array.from(betas)]
171
+ : existingBetas;
172
+
173
+ bedrockOptions.additionalModelRequestFields = {
174
+ ...bedrockOptions.additionalModelRequestFields,
175
+ anthropic_beta: mergedBetas,
176
+ };
177
+ }
178
+
179
+ const isAnthropicModel = this.modelId.includes('anthropic');
180
+ const isThinkingRequested =
181
+ bedrockOptions.reasoningConfig?.type === 'enabled';
182
+ const thinkingBudget = bedrockOptions.reasoningConfig?.budgetTokens;
183
+ const isAnthropicThinkingEnabled = isAnthropicModel && isThinkingRequested;
184
+
185
+ const inferenceConfig = {
186
+ ...(maxOutputTokens != null && { maxTokens: maxOutputTokens }),
187
+ ...(temperature != null && { temperature }),
188
+ ...(topP != null && { topP }),
189
+ ...(topK != null && { topK }),
190
+ ...(stopSequences != null && { stopSequences }),
191
+ };
192
+
193
+ if (isAnthropicThinkingEnabled && thinkingBudget != null) {
194
+ if (inferenceConfig.maxTokens != null) {
195
+ inferenceConfig.maxTokens += thinkingBudget;
196
+ } else {
197
+ inferenceConfig.maxTokens = thinkingBudget + 4096; // Default + thinking budget maxTokens = 4096, TODO update default in v5
198
+ }
199
+ // Add them to additional model request fields
200
+ // Add thinking config to additionalModelRequestFields
201
+ bedrockOptions.additionalModelRequestFields = {
202
+ ...bedrockOptions.additionalModelRequestFields,
203
+ thinking: {
204
+ type: bedrockOptions.reasoningConfig?.type,
205
+ budget_tokens: thinkingBudget,
206
+ },
207
+ };
208
+ } else if (!isAnthropicModel && thinkingBudget != null) {
209
+ warnings.push({
210
+ type: 'unsupported',
211
+ feature: 'budgetTokens',
212
+ details:
213
+ 'budgetTokens applies only to Anthropic models on Bedrock and will be ignored for this model.',
214
+ });
215
+ }
216
+
217
+ const maxReasoningEffort =
218
+ bedrockOptions.reasoningConfig?.maxReasoningEffort;
219
+ const isOpenAIModel = this.modelId.startsWith('openai.');
220
+
221
+ if (maxReasoningEffort != null && !isAnthropicModel) {
222
+ if (isOpenAIModel) {
223
+ // OpenAI models on Bedrock expect `reasoning_effort` as a flat value
224
+ bedrockOptions.additionalModelRequestFields = {
225
+ ...bedrockOptions.additionalModelRequestFields,
226
+ reasoning_effort: maxReasoningEffort,
227
+ };
228
+ } else {
229
+ // other models (such as Nova 2) use reasoningConfig format
230
+ bedrockOptions.additionalModelRequestFields = {
231
+ ...bedrockOptions.additionalModelRequestFields,
232
+ reasoningConfig: {
233
+ ...(bedrockOptions.reasoningConfig?.type != null && {
234
+ type: bedrockOptions.reasoningConfig.type,
235
+ }),
236
+ maxReasoningEffort,
237
+ },
238
+ };
239
+ }
240
+ } else if (maxReasoningEffort != null && isAnthropicModel) {
241
+ warnings.push({
242
+ type: 'unsupported',
243
+ feature: 'maxReasoningEffort',
244
+ details:
245
+ 'maxReasoningEffort applies only to Amazon Nova models on Bedrock and will be ignored for this model.',
246
+ });
247
+ }
248
+
249
+ if (isAnthropicThinkingEnabled && inferenceConfig.temperature != null) {
250
+ delete inferenceConfig.temperature;
251
+ warnings.push({
252
+ type: 'unsupported',
253
+ feature: 'temperature',
254
+ details: 'temperature is not supported when thinking is enabled',
255
+ });
256
+ }
257
+
258
+ if (isAnthropicThinkingEnabled && inferenceConfig.topP != null) {
259
+ delete inferenceConfig.topP;
260
+ warnings.push({
261
+ type: 'unsupported',
262
+ feature: 'topP',
263
+ details: 'topP is not supported when thinking is enabled',
264
+ });
265
+ }
266
+
267
+ if (isAnthropicThinkingEnabled && inferenceConfig.topK != null) {
268
+ delete inferenceConfig.topK;
269
+ warnings.push({
270
+ type: 'unsupported',
271
+ feature: 'topK',
272
+ details: 'topK is not supported when thinking is enabled',
273
+ });
274
+ }
275
+
276
+ // Filter tool content from prompt when no tools are available
277
+ const hasAnyTools = (toolConfig.tools?.length ?? 0) > 0 || additionalTools;
278
+ let filteredPrompt = prompt;
279
+
280
+ if (!hasAnyTools) {
281
+ const hasToolContent = prompt.some(
282
+ message =>
283
+ 'content' in message &&
284
+ Array.isArray(message.content) &&
285
+ message.content.some(
286
+ part => part.type === 'tool-call' || part.type === 'tool-result',
287
+ ),
288
+ );
289
+
290
+ if (hasToolContent) {
291
+ filteredPrompt = prompt
292
+ .map(message =>
293
+ message.role === 'system'
294
+ ? message
295
+ : {
296
+ ...message,
297
+ content: message.content.filter(
298
+ part =>
299
+ part.type !== 'tool-call' && part.type !== 'tool-result',
300
+ ),
301
+ },
302
+ )
303
+ .filter(
304
+ message => message.role === 'system' || message.content.length > 0,
305
+ ) as typeof prompt;
306
+
307
+ warnings.push({
308
+ type: 'unsupported',
309
+ feature: 'toolContent',
310
+ details:
311
+ 'Tool calls and results removed from conversation because Bedrock does not support tool content without active tools.',
312
+ });
313
+ }
314
+ }
315
+
316
+ const isMistral = isMistralModel(this.modelId);
317
+ const { system, messages } = await convertToBedrockChatMessages(
318
+ filteredPrompt,
319
+ isMistral,
320
+ );
321
+
322
+ // Filter out reasoningConfig from providerOptions.bedrock to prevent sending it to Bedrock API
323
+ const {
324
+ reasoningConfig: _,
325
+ additionalModelRequestFields: __,
326
+ ...filteredBedrockOptions
327
+ } = providerOptions?.bedrock || {};
328
+
329
+ const additionalModelResponseFieldPaths = isAnthropicModel
330
+ ? ['/delta/stop_sequence']
331
+ : undefined;
332
+
333
+ return {
334
+ command: {
335
+ system,
336
+ messages,
337
+ additionalModelRequestFields:
338
+ bedrockOptions.additionalModelRequestFields,
339
+ ...(additionalModelResponseFieldPaths && {
340
+ additionalModelResponseFieldPaths,
341
+ }),
342
+ ...(Object.keys(inferenceConfig).length > 0 && {
343
+ inferenceConfig,
344
+ }),
345
+ ...filteredBedrockOptions,
346
+ ...(toolConfig.tools !== undefined && toolConfig.tools.length > 0
347
+ ? { toolConfig }
348
+ : {}),
349
+ },
350
+ warnings,
351
+ usesJsonResponseTool: jsonResponseTool != null,
352
+ betas,
353
+ };
354
+ }
355
+
356
+ readonly supportedUrls: Record<string, RegExp[]> = {
357
+ // no supported urls for bedrock
358
+ };
359
+
360
+ private async getHeaders({
361
+ headers,
362
+ }: {
363
+ headers: Record<string, string | undefined> | undefined;
364
+ }) {
365
+ return combineHeaders(await resolve(this.config.headers), headers);
366
+ }
367
+
368
+ async doGenerate(
369
+ options: LanguageModelV3CallOptions,
370
+ ): Promise<LanguageModelV3GenerateResult> {
371
+ const {
372
+ command: args,
373
+ warnings,
374
+ usesJsonResponseTool,
375
+ } = await this.getArgs(options);
376
+
377
+ const url = `${this.getUrl(this.modelId)}/converse`;
378
+ const { value: response, responseHeaders } = await postJsonToApi({
379
+ url,
380
+ headers: await this.getHeaders({ headers: options.headers }),
381
+ body: args,
382
+ failedResponseHandler: createJsonErrorResponseHandler({
383
+ errorSchema: BedrockErrorSchema,
384
+ errorToMessage: error => `${error.message ?? 'Unknown error'}`,
385
+ }),
386
+ successfulResponseHandler: createJsonResponseHandler(
387
+ BedrockResponseSchema,
388
+ ),
389
+ abortSignal: options.abortSignal,
390
+ fetch: this.config.fetch,
391
+ });
392
+
393
+ const content: Array<LanguageModelV3Content> = [];
394
+ let isJsonResponseFromTool = false;
395
+
396
+ // map response content to content array
397
+ for (const part of response.output.message.content) {
398
+ // text
399
+ if (part.text) {
400
+ content.push({ type: 'text', text: part.text });
401
+ }
402
+
403
+ // reasoning
404
+ if (part.reasoningContent) {
405
+ if ('reasoningText' in part.reasoningContent) {
406
+ const reasoning: LanguageModelV3Reasoning = {
407
+ type: 'reasoning',
408
+ text: part.reasoningContent.reasoningText.text,
409
+ };
410
+
411
+ if (part.reasoningContent.reasoningText.signature) {
412
+ reasoning.providerMetadata = {
413
+ bedrock: {
414
+ signature: part.reasoningContent.reasoningText.signature,
415
+ } satisfies BedrockReasoningMetadata,
416
+ };
417
+ }
418
+
419
+ content.push(reasoning);
420
+ } else if ('redactedReasoning' in part.reasoningContent) {
421
+ content.push({
422
+ type: 'reasoning',
423
+ text: '',
424
+ providerMetadata: {
425
+ bedrock: {
426
+ redactedData:
427
+ part.reasoningContent.redactedReasoning.data ?? '',
428
+ } satisfies BedrockReasoningMetadata,
429
+ },
430
+ });
431
+ }
432
+ }
433
+
434
+ // tool calls
435
+ if (part.toolUse) {
436
+ const isJsonResponseTool =
437
+ usesJsonResponseTool && part.toolUse.name === 'json';
438
+
439
+ if (isJsonResponseTool) {
440
+ isJsonResponseFromTool = true;
441
+ // when a json response tool is used, the tool call becomes the text:
442
+ content.push({
443
+ type: 'text',
444
+ text: JSON.stringify(part.toolUse.input),
445
+ });
446
+ } else {
447
+ const isMistral = isMistralModel(this.modelId);
448
+ const rawToolCallId =
449
+ part.toolUse?.toolUseId ?? this.config.generateId();
450
+ content.push({
451
+ type: 'tool-call' as const,
452
+ toolCallId: normalizeToolCallId(rawToolCallId, isMistral),
453
+ toolName: part.toolUse?.name ?? `tool-${this.config.generateId()}`,
454
+ input: JSON.stringify(part.toolUse?.input ?? {}),
455
+ });
456
+ }
457
+ }
458
+ }
459
+
460
+ // provider metadata:
461
+ const stopSequence =
462
+ response.additionalModelResponseFields?.delta?.stop_sequence ?? null;
463
+
464
+ const providerMetadata =
465
+ response.trace || response.usage || isJsonResponseFromTool || stopSequence
466
+ ? {
467
+ bedrock: {
468
+ ...(response.trace && typeof response.trace === 'object'
469
+ ? { trace: response.trace as JSONObject }
470
+ : {}),
471
+ ...(response.usage?.cacheWriteInputTokens != null && {
472
+ usage: {
473
+ cacheWriteInputTokens: response.usage.cacheWriteInputTokens,
474
+ },
475
+ }),
476
+ ...(isJsonResponseFromTool && { isJsonResponseFromTool: true }),
477
+ stopSequence,
478
+ },
479
+ }
480
+ : undefined;
481
+
482
+ return {
483
+ content,
484
+ finishReason: {
485
+ unified: mapBedrockFinishReason(
486
+ response.stopReason as BedrockStopReason,
487
+ isJsonResponseFromTool,
488
+ ),
489
+ raw: response.stopReason ?? undefined,
490
+ },
491
+ usage: convertBedrockUsage(response.usage),
492
+ response: {
493
+ // TODO add id, timestamp, etc
494
+ headers: responseHeaders,
495
+ },
496
+ warnings,
497
+ ...(providerMetadata && { providerMetadata }),
498
+ };
499
+ }
500
+
501
+ async doStream(
502
+ options: LanguageModelV3CallOptions,
503
+ ): Promise<LanguageModelV3StreamResult> {
504
+ const {
505
+ command: args,
506
+ warnings,
507
+ usesJsonResponseTool,
508
+ } = await this.getArgs(options);
509
+ const isMistral = isMistralModel(this.modelId);
510
+ const url = `${this.getUrl(this.modelId)}/converse-stream`;
511
+
512
+ const { value: response, responseHeaders } = await postJsonToApi({
513
+ url,
514
+ headers: await this.getHeaders({ headers: options.headers }),
515
+ body: args,
516
+ failedResponseHandler: createJsonErrorResponseHandler({
517
+ errorSchema: BedrockErrorSchema,
518
+ errorToMessage: error => `${error.type}: ${error.message}`,
519
+ }),
520
+ successfulResponseHandler:
521
+ createBedrockEventStreamResponseHandler(BedrockStreamSchema),
522
+ abortSignal: options.abortSignal,
523
+ fetch: this.config.fetch,
524
+ });
525
+
526
+ let finishReason: LanguageModelV3FinishReason = {
527
+ unified: 'other',
528
+ raw: undefined,
529
+ };
530
+ let usage: BedrockUsage | undefined = undefined;
531
+ let providerMetadata: SharedV3ProviderMetadata | undefined = undefined;
532
+ let isJsonResponseFromTool = false;
533
+ let stopSequence: string | null = null;
534
+
535
+ const contentBlocks: Record<
536
+ number,
537
+ | {
538
+ type: 'tool-call';
539
+ toolCallId: string;
540
+ toolName: string;
541
+ jsonText: string;
542
+ isJsonResponseTool?: boolean;
543
+ }
544
+ | { type: 'text' | 'reasoning' }
545
+ > = {};
546
+
547
+ return {
548
+ stream: response.pipeThrough(
549
+ new TransformStream<
550
+ ParseResult<z.infer<typeof BedrockStreamSchema>>,
551
+ LanguageModelV3StreamPart
552
+ >({
553
+ start(controller) {
554
+ controller.enqueue({ type: 'stream-start', warnings });
555
+ },
556
+
557
+ transform(chunk, controller) {
558
+ function enqueueError(bedrockError: Record<string, any>) {
559
+ finishReason = { unified: 'error', raw: undefined };
560
+ controller.enqueue({ type: 'error', error: bedrockError });
561
+ }
562
+
563
+ // Emit raw chunk if requested (before anything else)
564
+ if (options.includeRawChunks) {
565
+ controller.enqueue({ type: 'raw', rawValue: chunk.rawValue });
566
+ }
567
+
568
+ // handle failed chunk parsing / validation:
569
+ if (!chunk.success) {
570
+ enqueueError(chunk.error);
571
+ return;
572
+ }
573
+
574
+ const value = chunk.value;
575
+
576
+ // handle errors:
577
+ if (value.internalServerException) {
578
+ enqueueError(value.internalServerException);
579
+ return;
580
+ }
581
+ if (value.modelStreamErrorException) {
582
+ enqueueError(value.modelStreamErrorException);
583
+ return;
584
+ }
585
+ if (value.throttlingException) {
586
+ enqueueError(value.throttlingException);
587
+ return;
588
+ }
589
+ if (value.validationException) {
590
+ enqueueError(value.validationException);
591
+ return;
592
+ }
593
+
594
+ if (value.messageStop) {
595
+ finishReason = {
596
+ unified: mapBedrockFinishReason(
597
+ value.messageStop.stopReason as BedrockStopReason,
598
+ isJsonResponseFromTool,
599
+ ),
600
+ raw: value.messageStop.stopReason ?? undefined,
601
+ };
602
+ stopSequence =
603
+ value.messageStop.additionalModelResponseFields?.delta
604
+ ?.stop_sequence ?? null;
605
+ }
606
+
607
+ if (value.metadata) {
608
+ if (value.metadata.usage) {
609
+ usage = value.metadata.usage;
610
+ }
611
+
612
+ const cacheUsage =
613
+ value.metadata.usage?.cacheWriteInputTokens != null
614
+ ? {
615
+ usage: {
616
+ cacheWriteInputTokens:
617
+ value.metadata.usage.cacheWriteInputTokens,
618
+ },
619
+ }
620
+ : undefined;
621
+
622
+ const trace = value.metadata.trace
623
+ ? {
624
+ trace: value.metadata.trace as JSONObject,
625
+ }
626
+ : undefined;
627
+
628
+ if (cacheUsage || trace) {
629
+ providerMetadata = {
630
+ bedrock: {
631
+ ...cacheUsage,
632
+ ...trace,
633
+ },
634
+ };
635
+ }
636
+ }
637
+
638
+ if (
639
+ value.contentBlockStart?.contentBlockIndex != null &&
640
+ !value.contentBlockStart?.start?.toolUse
641
+ ) {
642
+ const blockIndex = value.contentBlockStart.contentBlockIndex;
643
+ contentBlocks[blockIndex] = { type: 'text' };
644
+ controller.enqueue({
645
+ type: 'text-start',
646
+ id: String(blockIndex),
647
+ });
648
+ }
649
+
650
+ if (
651
+ value.contentBlockDelta?.delta &&
652
+ 'text' in value.contentBlockDelta.delta &&
653
+ value.contentBlockDelta.delta.text
654
+ ) {
655
+ const blockIndex = value.contentBlockDelta.contentBlockIndex || 0;
656
+
657
+ if (contentBlocks[blockIndex] == null) {
658
+ contentBlocks[blockIndex] = { type: 'text' };
659
+
660
+ controller.enqueue({
661
+ type: 'text-start',
662
+ id: String(blockIndex),
663
+ });
664
+ }
665
+
666
+ controller.enqueue({
667
+ type: 'text-delta',
668
+ id: String(blockIndex),
669
+ delta: value.contentBlockDelta.delta.text,
670
+ });
671
+ }
672
+
673
+ if (value.contentBlockStop?.contentBlockIndex != null) {
674
+ const blockIndex = value.contentBlockStop.contentBlockIndex;
675
+ const contentBlock = contentBlocks[blockIndex];
676
+
677
+ if (contentBlock != null) {
678
+ if (contentBlock.type === 'reasoning') {
679
+ controller.enqueue({
680
+ type: 'reasoning-end',
681
+ id: String(blockIndex),
682
+ });
683
+ } else if (contentBlock.type === 'text') {
684
+ controller.enqueue({
685
+ type: 'text-end',
686
+ id: String(blockIndex),
687
+ });
688
+ } else if (contentBlock.type === 'tool-call') {
689
+ if (contentBlock.isJsonResponseTool) {
690
+ isJsonResponseFromTool = true;
691
+ // when this specific tool is the json response tool, emit the tool input as text
692
+ controller.enqueue({
693
+ type: 'text-start',
694
+ id: String(blockIndex),
695
+ });
696
+ controller.enqueue({
697
+ type: 'text-delta',
698
+ id: String(blockIndex),
699
+ delta: contentBlock.jsonText,
700
+ });
701
+ controller.enqueue({
702
+ type: 'text-end',
703
+ id: String(blockIndex),
704
+ });
705
+ } else {
706
+ controller.enqueue({
707
+ type: 'tool-input-end',
708
+ id: contentBlock.toolCallId,
709
+ });
710
+ controller.enqueue({
711
+ type: 'tool-call',
712
+ toolCallId: contentBlock.toolCallId,
713
+ toolName: contentBlock.toolName,
714
+ input:
715
+ contentBlock.jsonText === ''
716
+ ? '{}'
717
+ : contentBlock.jsonText,
718
+ });
719
+ }
720
+ }
721
+
722
+ delete contentBlocks[blockIndex];
723
+ }
724
+ }
725
+
726
+ if (
727
+ value.contentBlockDelta?.delta &&
728
+ 'reasoningContent' in value.contentBlockDelta.delta &&
729
+ value.contentBlockDelta.delta.reasoningContent
730
+ ) {
731
+ const blockIndex = value.contentBlockDelta.contentBlockIndex || 0;
732
+ const reasoningContent =
733
+ value.contentBlockDelta.delta.reasoningContent;
734
+
735
+ if ('text' in reasoningContent && reasoningContent.text) {
736
+ if (contentBlocks[blockIndex] == null) {
737
+ contentBlocks[blockIndex] = { type: 'reasoning' };
738
+ controller.enqueue({
739
+ type: 'reasoning-start',
740
+ id: String(blockIndex),
741
+ });
742
+ }
743
+
744
+ controller.enqueue({
745
+ type: 'reasoning-delta',
746
+ id: String(blockIndex),
747
+ delta: reasoningContent.text,
748
+ });
749
+ } else if (
750
+ 'signature' in reasoningContent &&
751
+ reasoningContent.signature
752
+ ) {
753
+ controller.enqueue({
754
+ type: 'reasoning-delta',
755
+ id: String(blockIndex),
756
+ delta: '',
757
+ providerMetadata: {
758
+ bedrock: {
759
+ signature: reasoningContent.signature,
760
+ } satisfies BedrockReasoningMetadata,
761
+ },
762
+ });
763
+ } else if ('data' in reasoningContent && reasoningContent.data) {
764
+ controller.enqueue({
765
+ type: 'reasoning-delta',
766
+ id: String(blockIndex),
767
+ delta: '',
768
+ providerMetadata: {
769
+ bedrock: {
770
+ redactedData: reasoningContent.data,
771
+ } satisfies BedrockReasoningMetadata,
772
+ },
773
+ });
774
+ }
775
+ }
776
+
777
+ const contentBlockStart = value.contentBlockStart;
778
+ if (contentBlockStart?.start?.toolUse != null) {
779
+ const toolUse = contentBlockStart.start.toolUse;
780
+ const blockIndex = contentBlockStart.contentBlockIndex!;
781
+ const isJsonResponseTool =
782
+ usesJsonResponseTool && toolUse.name === 'json';
783
+
784
+ const normalizedToolCallId = normalizeToolCallId(
785
+ toolUse.toolUseId!,
786
+ isMistral,
787
+ );
788
+ contentBlocks[blockIndex] = {
789
+ type: 'tool-call',
790
+ toolCallId: normalizedToolCallId,
791
+ toolName: toolUse.name!,
792
+ jsonText: '',
793
+ isJsonResponseTool,
794
+ };
795
+
796
+ // when this specific tool is the json response tool, we don't emit tool events
797
+ if (!isJsonResponseTool) {
798
+ controller.enqueue({
799
+ type: 'tool-input-start',
800
+ id: normalizedToolCallId,
801
+ toolName: toolUse.name!,
802
+ });
803
+ }
804
+ }
805
+
806
+ const contentBlockDelta = value.contentBlockDelta;
807
+ if (
808
+ contentBlockDelta?.delta &&
809
+ 'toolUse' in contentBlockDelta.delta &&
810
+ contentBlockDelta.delta.toolUse
811
+ ) {
812
+ const blockIndex = contentBlockDelta.contentBlockIndex!;
813
+ const contentBlock = contentBlocks[blockIndex];
814
+
815
+ if (contentBlock?.type === 'tool-call') {
816
+ const delta = contentBlockDelta.delta.toolUse.input ?? '';
817
+
818
+ // when this specific tool is the json response tool, we don't emit tool events
819
+ if (!contentBlock.isJsonResponseTool) {
820
+ controller.enqueue({
821
+ type: 'tool-input-delta',
822
+ id: contentBlock.toolCallId,
823
+ delta: delta,
824
+ });
825
+ }
826
+
827
+ contentBlock.jsonText += delta;
828
+ }
829
+ }
830
+ },
831
+ flush(controller) {
832
+ // Update provider metadata with isJsonResponseFromTool and stopSequence if needed
833
+ if (isJsonResponseFromTool || stopSequence != null) {
834
+ if (providerMetadata) {
835
+ providerMetadata.bedrock = {
836
+ ...providerMetadata.bedrock,
837
+ ...(isJsonResponseFromTool && {
838
+ isJsonResponseFromTool: true,
839
+ }),
840
+ stopSequence,
841
+ };
842
+ } else {
843
+ providerMetadata = {
844
+ bedrock: {
845
+ ...(isJsonResponseFromTool && {
846
+ isJsonResponseFromTool: true,
847
+ }),
848
+ stopSequence,
849
+ },
850
+ };
851
+ }
852
+ }
853
+
854
+ controller.enqueue({
855
+ type: 'finish',
856
+ finishReason,
857
+ usage: convertBedrockUsage(usage),
858
+ ...(providerMetadata && { providerMetadata }),
859
+ });
860
+ },
861
+ }),
862
+ ),
863
+ // TODO request?
864
+ response: { headers: responseHeaders },
865
+ };
866
+ }
867
+
868
+ private getUrl(modelId: string) {
869
+ const encodedModelId = encodeURIComponent(modelId);
870
+ return `${this.config.baseUrl()}/model/${encodedModelId}`;
871
+ }
872
+ }
873
+
874
+ const BedrockStopReasonSchema = z.union([
875
+ z.enum(BEDROCK_STOP_REASONS),
876
+ z.string(),
877
+ ]);
878
+
879
+ const BedrockAdditionalModelResponseFieldsSchema = z
880
+ .object({
881
+ delta: z
882
+ .object({
883
+ stop_sequence: z.string().nullish(),
884
+ })
885
+ .nullish(),
886
+ })
887
+ .catchall(z.unknown());
888
+
889
+ const BedrockToolUseSchema = z.object({
890
+ toolUseId: z.string(),
891
+ name: z.string(),
892
+ input: z.unknown(),
893
+ });
894
+
895
+ const BedrockReasoningTextSchema = z.object({
896
+ signature: z.string().nullish(),
897
+ text: z.string(),
898
+ });
899
+
900
+ const BedrockRedactedReasoningSchema = z.object({
901
+ data: z.string(),
902
+ });
903
+
904
+ // limited version of the schema, focused on what is needed for the implementation
905
+ // this approach limits breakages when the API changes and increases efficiency
906
+ const BedrockResponseSchema = z.object({
907
+ metrics: z
908
+ .object({
909
+ latencyMs: z.number(),
910
+ })
911
+ .nullish(),
912
+ output: z.object({
913
+ message: z.object({
914
+ content: z.array(
915
+ z.object({
916
+ text: z.string().nullish(),
917
+ toolUse: BedrockToolUseSchema.nullish(),
918
+ reasoningContent: z
919
+ .union([
920
+ z.object({
921
+ reasoningText: BedrockReasoningTextSchema,
922
+ }),
923
+ z.object({
924
+ redactedReasoning: BedrockRedactedReasoningSchema,
925
+ }),
926
+ ])
927
+ .nullish(),
928
+ }),
929
+ ),
930
+ role: z.string(),
931
+ }),
932
+ }),
933
+ stopReason: BedrockStopReasonSchema,
934
+ additionalModelResponseFields:
935
+ BedrockAdditionalModelResponseFieldsSchema.nullish(),
936
+ trace: z.unknown().nullish(),
937
+ usage: z.object({
938
+ inputTokens: z.number(),
939
+ outputTokens: z.number(),
940
+ totalTokens: z.number(),
941
+ cacheReadInputTokens: z.number().nullish(),
942
+ cacheWriteInputTokens: z.number().nullish(),
943
+ }),
944
+ });
945
+
946
+ // limited version of the schema, focussed on what is needed for the implementation
947
+ // this approach limits breakages when the API changes and increases efficiency
948
+ const BedrockStreamSchema = z.object({
949
+ contentBlockDelta: z
950
+ .object({
951
+ contentBlockIndex: z.number(),
952
+ delta: z
953
+ .union([
954
+ z.object({ text: z.string() }),
955
+ z.object({ toolUse: z.object({ input: z.string() }) }),
956
+ z.object({
957
+ reasoningContent: z.object({ text: z.string() }),
958
+ }),
959
+ z.object({
960
+ reasoningContent: z.object({
961
+ signature: z.string(),
962
+ }),
963
+ }),
964
+ z.object({
965
+ reasoningContent: z.object({ data: z.string() }),
966
+ }),
967
+ ])
968
+ .nullish(),
969
+ })
970
+ .nullish(),
971
+ contentBlockStart: z
972
+ .object({
973
+ contentBlockIndex: z.number(),
974
+ start: z
975
+ .object({
976
+ toolUse: BedrockToolUseSchema.nullish(),
977
+ })
978
+ .nullish(),
979
+ })
980
+ .nullish(),
981
+ contentBlockStop: z
982
+ .object({
983
+ contentBlockIndex: z.number(),
984
+ })
985
+ .nullish(),
986
+ internalServerException: z.record(z.string(), z.unknown()).nullish(),
987
+ messageStop: z
988
+ .object({
989
+ additionalModelResponseFields:
990
+ BedrockAdditionalModelResponseFieldsSchema.nullish(),
991
+ stopReason: BedrockStopReasonSchema,
992
+ })
993
+ .nullish(),
994
+ metadata: z
995
+ .object({
996
+ trace: z.unknown().nullish(),
997
+ usage: z
998
+ .object({
999
+ cacheReadInputTokens: z.number().nullish(),
1000
+ cacheWriteInputTokens: z.number().nullish(),
1001
+ inputTokens: z.number(),
1002
+ outputTokens: z.number(),
1003
+ })
1004
+ .nullish(),
1005
+ })
1006
+ .nullish(),
1007
+ modelStreamErrorException: z.record(z.string(), z.unknown()).nullish(),
1008
+ throttlingException: z.record(z.string(), z.unknown()).nullish(),
1009
+ validationException: z.record(z.string(), z.unknown()).nullish(),
1010
+ });
1011
+
1012
+ export const bedrockReasoningMetadataSchema = z.object({
1013
+ signature: z.string().optional(),
1014
+ redactedData: z.string().optional(),
1015
+ });
1016
+
1017
+ export type BedrockReasoningMetadata = z.infer<
1018
+ typeof bedrockReasoningMetadataSchema
1019
+ >;