diff --git a/.changeset/wet-geese-remain.md b/.changeset/wet-geese-remain.md new file mode 100644 index 000000000000..84d54a812121 --- /dev/null +++ b/.changeset/wet-geese-remain.md @@ -0,0 +1,5 @@ +--- +'@ai-sdk/amazon-bedrock': patch +--- + +Fix empty responses when bedrock claude citations object is returned diff --git a/examples/ai-core/src/generate-text/amazon-bedrock-citations.ts b/examples/ai-core/src/generate-text/amazon-bedrock-citations.ts new file mode 100644 index 000000000000..1461c1d113fa --- /dev/null +++ b/examples/ai-core/src/generate-text/amazon-bedrock-citations.ts @@ -0,0 +1,58 @@ +import { bedrock } from '@ai-sdk/amazon-bedrock'; +import { generateText } from 'ai'; +import 'dotenv/config'; +import { BedrockCitation } from '../../../../packages/amazon-bedrock/src/bedrock-api-types'; + +async function main() { + const result = await generateText({ + model: bedrock('anthropic.claude-3-7-sonnet-20250219-v1:0'), + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: 'What color is the grass? Use citations.', + }, + { + type: 'file', + mediaType: 'text/plain', + data: 'The grass is green in spring and summer. The sky is blue during clear weather.', + providerOptions: { + bedrock: { + citations: { enabled: true }, + }, + }, + }, + ], + }, + ], + }); + + console.log('Response:', result.text); + + const citations = result.content.filter(part => part.type === 'source'); + citations.forEach((citation, i) => { + if ( + citation.sourceType === 'document' && + citation.providerMetadata?.bedrock + ) { + const metaCitation = citation.providerMetadata.bedrock + .citation as BedrockCitation; + if (!metaCitation) { + return; + } + + const citedText = metaCitation.sourceContent?.[0]?.text ?? 'N/A'; + const location = + metaCitation.location?.documentChar || + metaCitation.location?.documentPage || + metaCitation.location?.documentChunk; + const startIdx = location?.start ?? 'N/A'; + const endIdx = location?.end ?? 'N/A'; + console.log(`\n[${i + 1}] "${citedText}" (chars: ${startIdx}-${endIdx})`); + } + }); +} + +main().catch(console.error); diff --git a/packages/amazon-bedrock/src/bedrock-api-types.ts b/packages/amazon-bedrock/src/bedrock-api-types.ts index 6e75f32bfe98..92b844f1b622 100644 --- a/packages/amazon-bedrock/src/bedrock-api-types.ts +++ b/packages/amazon-bedrock/src/bedrock-api-types.ts @@ -182,6 +182,37 @@ export interface BedrockRedactedReasoningContentBlock { }; } +export interface BedrockCitationLocation { + documentChar?: { + documentIndex: number; + start: number; + end: number; + } | null; + documentPage?: { + documentIndex: number; + start: number; + end: number; + } | null; + documentChunk?: { + documentIndex: number; + start: number; + end: number; + } | null; +} + +export interface BedrockCitation { + title?: string | null; + location?: BedrockCitationLocation | null; + sourceContent?: Array<{ text: string }> | null; +} + +export interface BedrockCitationsContentBlock { + citationsContent: { + content: Array<{ text: string }>; + citations: Array; + }; +} + export type BedrockContentBlock = | BedrockDocumentBlock | BedrockGuardrailConverseContentBlock @@ -191,4 +222,5 @@ export type BedrockContentBlock = | BedrockToolUseBlock | BedrockReasoningContentBlock | BedrockRedactedReasoningContentBlock + | BedrockCitationsContentBlock | BedrockCachePoint; diff --git a/packages/amazon-bedrock/src/bedrock-chat-language-model.test.ts b/packages/amazon-bedrock/src/bedrock-chat-language-model.test.ts index 5ede18a53a87..b977e8c4a83e 100644 --- a/packages/amazon-bedrock/src/bedrock-chat-language-model.test.ts +++ b/packages/amazon-bedrock/src/bedrock-chat-language-model.test.ts @@ -7,6 +7,7 @@ import { injectFetchHeaders } from './inject-fetch-headers'; import { BedrockReasoningContentBlock, BedrockRedactedReasoningContentBlock, + BedrockCitationsContentBlock, } from './bedrock-api-types'; import { anthropicTools, prepareTools } from '@ai-sdk/anthropic/internal'; import { z } from 'zod/v4'; @@ -1389,6 +1390,153 @@ describe('doStream', () => { `); }); + it('should process PDF citation responses in streaming', async () => { + setupMockEventStreamHandler(); + server.urls[streamUrl].response = { + type: 'stream-chunks', + chunks: [ + JSON.stringify({ + contentBlockDelta: { + contentBlockIndex: 0, + delta: { text: 'Based on the document' }, + }, + }) + '\n', + JSON.stringify({ + contentBlockDelta: { + contentBlockIndex: 0, + delta: { text: ', results show growth.' }, + }, + }) + '\n', + JSON.stringify({ + contentBlockStop: { contentBlockIndex: 0 }, + }) + '\n', + JSON.stringify({ + contentBlockDelta: { + contentBlockIndex: 0, + delta: { + citation: { + title: 'Financial Report 2023', + location: { + documentPage: { + documentIndex: 0, + start: 5, + end: 5, + }, + }, + sourceContent: [ + { + text: 'Revenue increased by 25% year over year', + }, + ], + }, + }, + }, + }) + '\n', + JSON.stringify({ + metadata: { + usage: { inputTokens: 17, outputTokens: 227, totalTokens: 244 }, + }, + }) + '\n', + JSON.stringify({ + messageStop: { + stopReason: 'end_turn', + }, + }) + '\n', + ], + }; + + const { stream } = await model.doStream({ + prompt: [ + { + role: 'user', + content: [ + { + type: 'file', + data: 'base64PDFdata', + mediaType: 'application/pdf', + filename: 'financial-report.pdf', + providerOptions: { + bedrock: { + citations: { enabled: true }, + }, + }, + }, + { + type: 'text', + text: 'What do the results show?', + }, + ], + }, + ], + }); + + const result = await convertReadableStreamToArray(stream); + + expect(result).toMatchInlineSnapshot(` + [ + { + "type": "stream-start", + "warnings": [], + }, + { + "id": "0", + "type": "text-start", + }, + { + "delta": "Based on the document", + "id": "0", + "type": "text-delta", + }, + { + "delta": ", results show growth.", + "id": "0", + "type": "text-delta", + }, + { + "id": "0", + "type": "text-end", + }, + { + "filename": "financial-report.pdf", + "id": "test-id", + "mediaType": "application/pdf", + "providerMetadata": { + "bedrock": { + "citation": { + "location": { + "documentPage": { + "documentIndex": 0, + "end": 5, + "start": 5, + }, + }, + "sourceContent": [ + { + "text": "Revenue increased by 25% year over year", + }, + ], + "title": "Financial Report 2023", + }, + }, + }, + "sourceType": "document", + "title": "Financial Report 2023", + "type": "source", + }, + { + "finishReason": "stop", + "type": "finish", + "usage": { + "cachedInputTokens": undefined, + "inputTokens": 17, + "outputTokens": 227, + "totalTokens": 244, + }, + }, + ] + `); + }); + it('should transform reasoningConfig to thinking in stream requests', async () => { setupMockEventStreamHandler(); server.urls[streamUrl].response = { @@ -1577,6 +1725,7 @@ describe('doGenerate', () => { | { type: 'tool_use'; id: string; name: string; input: unknown } | BedrockReasoningContentBlock | BedrockRedactedReasoningContentBlock + | BedrockCitationsContentBlock >; toolCalls?: Array<{ id?: string; @@ -2462,6 +2611,648 @@ describe('doGenerate', () => { ]); }); + it('should process PDF citation responses in generate', async () => { + prepareJsonResponse({ + content: [ + { + citationsContent: { + content: [ + { + text: 'Based on the financial report, the company showed significant growth this year.', + }, + ], + citations: [ + { + title: 'Financial Report 2023', + location: { + documentPage: { + documentIndex: 0, + start: 5, + end: 5, + }, + }, + sourceContent: [ + { + text: 'Revenue increased by 25% year over year', + }, + ], + }, + ], + }, + }, + ], + }); + + const result = await model.doGenerate({ + prompt: [ + { + role: 'user', + content: [ + { + type: 'file', + data: 'base64PDFdata', + mediaType: 'application/pdf', + filename: 'financial-report.pdf', + providerOptions: { + bedrock: { + citations: { enabled: true }, + }, + }, + }, + { + type: 'text', + text: 'What do the results show?', + }, + ], + }, + ], + }); + + expect(result.content).toMatchInlineSnapshot(` + [ + { + "text": "Based on the financial report, the company showed significant growth this year.", + "type": "text", + }, + { + "filename": "financial-report.pdf", + "id": "test-id", + "mediaType": "application/pdf", + "providerMetadata": { + "bedrock": { + "citation": { + "location": { + "documentPage": { + "documentIndex": 0, + "end": 5, + "start": 5, + }, + }, + "sourceContent": [ + { + "text": "Revenue increased by 25% year over year", + }, + ], + "title": "Financial Report 2023", + }, + }, + }, + "sourceType": "document", + "title": "Financial Report 2023", + "type": "source", + }, + ] + `); + }); + + it('should process multiple citations in generate', async () => { + prepareJsonResponse({ + content: [ + { + citationsContent: { + content: [ + { + text: 'The annual report and quarterly review both show positive trends.', + }, + ], + citations: [ + { + title: 'Annual Report 2023', + location: { + documentPage: { + documentIndex: 0, + start: 3, + end: 3, + }, + }, + sourceContent: [ + { + text: 'Overall revenue growth of 30%', + }, + ], + }, + { + title: 'Q4 Quarterly Review', + location: { + documentPage: { + documentIndex: 1, + start: 1, + end: 1, + }, + }, + sourceContent: [ + { + text: 'Q4 performance exceeded expectations', + }, + ], + }, + ], + }, + }, + ], + }); + + const result = await model.doGenerate({ + prompt: [ + { + role: 'user', + content: [ + { + type: 'file', + data: 'base64PDFdata1', + mediaType: 'application/pdf', + filename: 'annual-report.pdf', + providerOptions: { + bedrock: { + citations: { enabled: true }, + }, + }, + }, + { + type: 'file', + data: 'base64PDFdata2', + mediaType: 'application/pdf', + filename: 'quarterly-review.pdf', + providerOptions: { + bedrock: { + citations: { enabled: true }, + }, + }, + }, + { + type: 'text', + text: 'Summarize the performance data.', + }, + ], + }, + ], + }); + + expect(result.content).toMatchInlineSnapshot(` + [ + { + "text": "The annual report and quarterly review both show positive trends.", + "type": "text", + }, + { + "filename": "annual-report.pdf", + "id": "test-id", + "mediaType": "application/pdf", + "providerMetadata": { + "bedrock": { + "citation": { + "location": { + "documentPage": { + "documentIndex": 0, + "end": 3, + "start": 3, + }, + }, + "sourceContent": [ + { + "text": "Overall revenue growth of 30%", + }, + ], + "title": "Annual Report 2023", + }, + }, + }, + "sourceType": "document", + "title": "Annual Report 2023", + "type": "source", + }, + { + "filename": "quarterly-review.pdf", + "id": "test-id", + "mediaType": "application/pdf", + "providerMetadata": { + "bedrock": { + "citation": { + "location": { + "documentPage": { + "documentIndex": 1, + "end": 1, + "start": 1, + }, + }, + "sourceContent": [ + { + "text": "Q4 performance exceeded expectations", + }, + ], + "title": "Q4 Quarterly Review", + }, + }, + }, + "sourceType": "document", + "title": "Q4 Quarterly Review", + "type": "source", + }, + ] + `); + }); + + it('should handle citations with character-based location in generate', async () => { + prepareJsonResponse({ + content: [ + { + citationsContent: { + content: [ + { + text: 'The research indicates promising results.', + }, + ], + citations: [ + { + title: 'Research Paper 2023', + location: { + documentChar: { + documentIndex: 0, + start: 1234, + end: 1345, + }, + }, + sourceContent: [ + { + text: 'Statistical significance was achieved with p < 0.001', + }, + ], + }, + ], + }, + }, + ], + }); + + const result = await model.doGenerate({ + prompt: [ + { + role: 'user', + content: [ + { + type: 'file', + data: 'base64PDFdata', + mediaType: 'application/pdf', + filename: 'research-paper.pdf', + providerOptions: { + bedrock: { + citations: { enabled: true }, + }, + }, + }, + { + type: 'text', + text: 'What does the research show?', + }, + ], + }, + ], + }); + + expect(result.content).toMatchInlineSnapshot(` + [ + { + "text": "The research indicates promising results.", + "type": "text", + }, + { + "filename": "research-paper.pdf", + "id": "test-id", + "mediaType": "application/pdf", + "providerMetadata": { + "bedrock": { + "citation": { + "location": { + "documentChar": { + "documentIndex": 0, + "end": 1345, + "start": 1234, + }, + }, + "sourceContent": [ + { + "text": "Statistical significance was achieved with p < 0.001", + }, + ], + "title": "Research Paper 2023", + }, + }, + }, + "sourceType": "document", + "title": "Research Paper 2023", + "type": "source", + }, + ] + `); + }); + + it('should handle citations without title in generate', async () => { + prepareJsonResponse({ + content: [ + { + citationsContent: { + content: [ + { + text: 'The document provides valuable insights.', + }, + ], + citations: [ + { + title: null, + location: { + documentPage: { + documentIndex: 0, + start: 2, + end: 2, + }, + }, + sourceContent: [ + { + text: 'Key findings from the analysis', + }, + ], + }, + ], + }, + }, + ], + }); + + const result = await model.doGenerate({ + prompt: [ + { + role: 'user', + content: [ + { + type: 'file', + data: 'base64PDFdata', + mediaType: 'application/pdf', + filename: 'analysis.pdf', + providerOptions: { + bedrock: { + citations: { enabled: true }, + }, + }, + }, + { + type: 'text', + text: 'Analyze this document.', + }, + ], + }, + ], + }); + + expect(result.content).toMatchInlineSnapshot(` + [ + { + "text": "The document provides valuable insights.", + "type": "text", + }, + { + "filename": "analysis.pdf", + "id": "test-id", + "mediaType": "application/pdf", + "providerMetadata": { + "bedrock": { + "citation": { + "location": { + "documentPage": { + "documentIndex": 0, + "end": 2, + "start": 2, + }, + }, + "sourceContent": [ + { + "text": "Key findings from the analysis", + }, + ], + "title": null, + }, + }, + }, + "sourceType": "document", + "title": "analysis.pdf", + "type": "source", + }, + ] + `); + }); + + it('should handle citations with mixed content types in generate', async () => { + prepareJsonResponse({ + content: [ + { + type: 'text', + text: 'First, let me summarize what I found.', + }, + { + citationsContent: { + content: [ + { + text: 'The data shows significant trends.', + }, + ], + citations: [ + { + title: 'Data Analysis Report', + location: { + documentPage: { + documentIndex: 0, + start: 7, + end: 7, + }, + }, + sourceContent: [ + { + text: 'Trend analysis reveals upward trajectory', + }, + ], + }, + ], + }, + }, + { + type: 'text', + text: 'In conclusion, the results are promising.', + }, + ], + }); + + const result = await model.doGenerate({ + prompt: [ + { + role: 'user', + content: [ + { + type: 'file', + data: 'base64PDFdata', + mediaType: 'application/pdf', + filename: 'data-report.pdf', + providerOptions: { + bedrock: { + citations: { enabled: true }, + }, + }, + }, + { + type: 'text', + text: 'What trends do you see?', + }, + ], + }, + ], + }); + + expect(result.content).toMatchInlineSnapshot(` + [ + { + "text": "First, let me summarize what I found.", + "type": "text", + }, + { + "text": "The data shows significant trends.", + "type": "text", + }, + { + "filename": "data-report.pdf", + "id": "test-id", + "mediaType": "application/pdf", + "providerMetadata": { + "bedrock": { + "citation": { + "location": { + "documentPage": { + "documentIndex": 0, + "end": 7, + "start": 7, + }, + }, + "sourceContent": [ + { + "text": "Trend analysis reveals upward trajectory", + }, + ], + "title": "Data Analysis Report", + }, + }, + }, + "sourceType": "document", + "title": "Data Analysis Report", + "type": "source", + }, + { + "text": "In conclusion, the results are promising.", + "type": "text", + }, + ] + `); + }); + + it('should skip citations with invalid location in generate', async () => { + prepareJsonResponse({ + content: [ + { + citationsContent: { + content: [ + { + text: 'Some content was found.', + }, + ], + citations: [ + { + title: 'Valid Citation', + location: { + documentPage: { + documentIndex: 0, + start: 1, + end: 1, + }, + }, + sourceContent: [ + { + text: 'Valid source content', + }, + ], + }, + { + title: 'Invalid Citation', + location: null, + sourceContent: [ + { + text: 'This should be skipped', + }, + ], + }, + ], + }, + }, + ], + }); + + const result = await model.doGenerate({ + prompt: [ + { + role: 'user', + content: [ + { + type: 'file', + data: 'base64PDFdata', + mediaType: 'application/pdf', + filename: 'mixed-content.pdf', + providerOptions: { + bedrock: { + citations: { enabled: true }, + }, + }, + }, + { + type: 'text', + text: 'Analyze this content.', + }, + ], + }, + ], + }); + + expect(result.content).toMatchInlineSnapshot(` + [ + { + "text": "Some content was found.", + "type": "text", + }, + { + "filename": "mixed-content.pdf", + "id": "test-id", + "mediaType": "application/pdf", + "providerMetadata": { + "bedrock": { + "citation": { + "location": { + "documentPage": { + "documentIndex": 0, + "end": 1, + "start": 1, + }, + }, + "sourceContent": [ + { + "text": "Valid source content", + }, + ], + "title": "Valid Citation", + }, + }, + }, + "sourceType": "document", + "title": "Valid Citation", + "type": "source", + }, + ] + `); + }); + it('should omit toolConfig when conversation has tool calls but toolChoice is none', async () => { prepareJsonResponse({}); diff --git a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts index 8f4190dbdabe..8561a8c43118 100644 --- a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts +++ b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts @@ -9,6 +9,8 @@ import { LanguageModelV3Usage, SharedV3ProviderMetadata, LanguageModelV3FunctionTool, + LanguageModelV3Prompt, + LanguageModelV3Source, } from '@ai-sdk/provider'; import { FetchFunction, @@ -17,6 +19,7 @@ import { combineHeaders, createJsonErrorResponseHandler, createJsonResponseHandler, + generateId, parseProviderOptions, postJsonToApi, resolve, @@ -37,6 +40,41 @@ import { prepareTools } from './bedrock-prepare-tools'; import { convertToBedrockChatMessages } from './convert-to-bedrock-chat-messages'; import { mapBedrockFinishReason } from './map-bedrock-finish-reason'; +function createCitationSource( + citation: z.infer, + citationDocuments: Array<{ + title: string; + filename?: string; + mediaType: string; + }>, + generateId: () => string, +): LanguageModelV3Source | undefined { + const location = + citation?.location?.documentPage || + citation?.location?.documentChar || + citation?.location?.documentChunk; + if (!location) { + return; + } + + const documentInfo = citationDocuments[location.documentIndex]; + if (!documentInfo) { + return; + } + + return { + type: 'source' as const, + sourceType: 'document' as const, + id: generateId(), + mediaType: documentInfo.mediaType, + title: citation.title ?? documentInfo.title, + filename: documentInfo.filename, + providerMetadata: { + bedrock: { citation }, + } satisfies SharedV3ProviderMetadata, + }; +} + type BedrockChatConfig = { baseUrl: () => string; headers: Resolvable>; @@ -48,10 +86,14 @@ export class BedrockChatLanguageModel implements LanguageModelV3 { readonly specificationVersion = 'v3'; readonly provider = 'amazon-bedrock'; + private readonly generateId: () => string; + constructor( readonly modelId: BedrockChatModelId, private readonly config: BedrockChatConfig, - ) {} + ) { + this.generateId = config.generateId ?? generateId; + } private async getArgs({ prompt, @@ -302,6 +344,49 @@ export class BedrockChatLanguageModel implements LanguageModelV3 { ); } + private extractCitationDocuments(prompt: LanguageModelV3Prompt): Array<{ + title: string; + filename?: string; + mediaType: string; + }> { + const isCitationPart = (part: { + type: string; + mediaType?: string; + providerOptions?: { bedrock?: { citations?: { enabled?: boolean } } }; + }) => { + if (part.type !== 'file') { + return false; + } + + if ( + part.mediaType !== 'application/pdf' && + part.mediaType !== 'text/plain' + ) { + return false; + } + + const bedrock = part.providerOptions?.bedrock; + const citationsConfig = bedrock?.citations as + | { enabled?: boolean } + | undefined; + return citationsConfig?.enabled ?? false; + }; + + return prompt + .filter(message => message.role === 'user') + .flatMap(message => message.content) + .filter(isCitationPart) + .map(part => { + // TypeScript knows this is a file part due to our filter + const filePart = part as Extract; + return { + title: filePart.filename ?? 'Untitled Document', + filename: filePart.filename, + mediaType: filePart.mediaType, + }; + }); + } + async doGenerate( options: Parameters[0], ): Promise>> { @@ -328,6 +413,9 @@ export class BedrockChatLanguageModel implements LanguageModelV3 { fetch: this.config.fetch, }); + // Extract citation documents for response processing + const citationDocuments = this.extractCitationDocuments(options.prompt); + const content: Array = []; // map response content to content array @@ -341,6 +429,30 @@ export class BedrockChatLanguageModel implements LanguageModelV3 { } } + // citations + if (part.citationsContent) { + // Push the generated content as text + for (const generatedContent of part.citationsContent.content) { + content.push({ + type: 'text', + text: generatedContent.text, + }); + } + + // Convert citations to source chunks + for (const citation of part.citationsContent.citations) { + const source = createCitationSource( + citation, + citationDocuments, + this.generateId, + ); + + if (source) { + content.push(source); + } + } + } + // reasoning if (part.reasoningContent) { if ('reasoningText' in part.reasoningContent) { @@ -441,6 +553,8 @@ export class BedrockChatLanguageModel implements LanguageModelV3 { } = await this.getArgs(options); const url = `${this.getUrl(this.modelId)}/converse-stream`; + // Extract citation documents for response processing + const citationDocuments = this.extractCitationDocuments(options.prompt); const { value: response, responseHeaders } = await postJsonToApi({ url, headers: await this.getHeaders({ betas, headers: options.headers }), @@ -474,6 +588,8 @@ export class BedrockChatLanguageModel implements LanguageModelV3 { | { type: 'text' | 'reasoning' } > = {}; + const generateId = this.generateId; + return { stream: response.pipeThrough( new TransformStream< @@ -608,6 +724,23 @@ export class BedrockChatLanguageModel implements LanguageModelV3 { } } + if ( + value.contentBlockDelta?.delta && + 'citation' in value.contentBlockDelta.delta && + value.contentBlockDelta.delta.citation + ) { + const citation = value.contentBlockDelta.delta.citation; + const source = createCitationSource( + citation, + citationDocuments, + generateId, + ); + + if (source) { + controller.enqueue(source); + } + } + if (value.contentBlockStop?.contentBlockIndex != null) { const blockIndex = value.contentBlockStop.contentBlockIndex; const contentBlock = contentBlocks[blockIndex]; @@ -798,6 +931,39 @@ const BedrockRedactedReasoningSchema = z.object({ data: z.string(), }); +const DocumentLocationSchema = z.object({ + documentIndex: z.number(), + start: z.number().min(0), + end: z.number().min(0), +}); + +const BedrockCitationLocationSchema = z.object({ + documentChar: DocumentLocationSchema.nullish(), + documentPage: DocumentLocationSchema.nullish(), + documentChunk: DocumentLocationSchema.nullish(), +}); + +const BedrockCitationSchema = z.object({ + title: z.string().nullish(), + sourceContent: z + .array( + z.object({ + text: z.string(), + }), + ) + .nullish(), + location: BedrockCitationLocationSchema.nullish(), +}); + +const BedrockCitationsContentSchema = z.object({ + content: z.array( + z.object({ + text: z.string(), + }), + ), + citations: z.array(BedrockCitationSchema), +}); + // limited version of the schema, focused on what is needed for the implementation // this approach limits breakages when the API changes and increases efficiency const BedrockResponseSchema = z.object({ @@ -812,6 +978,7 @@ const BedrockResponseSchema = z.object({ z.object({ text: z.string().nullish(), toolUse: BedrockToolUseSchema.nullish(), + citationsContent: BedrockCitationsContentSchema.nullish(), reasoningContent: z .union([ z.object({ @@ -859,6 +1026,9 @@ const BedrockStreamSchema = z.object({ z.object({ reasoningContent: z.object({ data: z.string() }), }), + z.object({ + citation: BedrockCitationSchema, + }), ]) .nullish(), })