From c0279784c81981bffec13ddf4561146cc2af5042 Mon Sep 17 00:00:00 2001 From: Lewis Wolfe Date: Mon, 27 Oct 2025 16:39:48 +0000 Subject: [PATCH 01/14] Create citations schema and implement push of text content as default response --- .../src/bedrock-chat-language-model.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts index 8f4190dbdabe..1d8c85160810 100644 --- a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts +++ b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts @@ -341,6 +341,23 @@ export class BedrockChatLanguageModel implements LanguageModelV3 { } } + // citations + if (part.citationsContent) { + for (const generatedContent of part.citationsContent.content) { + // Push the citation generated content (text block) as text to prevent an empty response when citations are present + content.push({ + type: 'text', + text: generatedContent.text, + // provide actual citations in providerMetadata + providerMetadata: { + bedrock: { + citations: part.citationsContent.citations + }, + }, + }); + } + } + // reasoning if (part.reasoningContent) { if ('reasoningText' in part.reasoningContent) { @@ -798,6 +815,38 @@ 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(), + sourceContent: z + .array( + z.object({ + text: z.string(), + }), + ), + location: BedrockCitationLocationSchema, +}); + +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 +861,7 @@ const BedrockResponseSchema = z.object({ z.object({ text: z.string().nullish(), toolUse: BedrockToolUseSchema.nullish(), + citationsContent: BedrockCitationsContentSchema.nullish(), reasoningContent: z .union([ z.object({ From c5949b53169494cc676bb7b89480913a9a75e351 Mon Sep 17 00:00:00 2001 From: Lewis Wolfe Date: Tue, 28 Oct 2025 16:45:07 +0000 Subject: [PATCH 02/14] fix citation error in bedrock stream schema --- .../amazon-bedrock/src/bedrock-chat-language-model.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts index 1d8c85160810..1defe1e986d0 100644 --- a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts +++ b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts @@ -828,14 +828,15 @@ const BedrockCitationLocationSchema = z.object({ }); const BedrockCitationSchema = z.object({ - title: z.string(), + title: z.string().nullish(), sourceContent: z .array( z.object({ text: z.string(), }), - ), - location: BedrockCitationLocationSchema, + ) + .nullish(), + location: BedrockCitationLocationSchema.nullish(), }); const BedrockCitationsContentSchema = z.object({ @@ -909,6 +910,9 @@ const BedrockStreamSchema = z.object({ z.object({ reasoningContent: z.object({ data: z.string() }), }), + z.object({ + citation: BedrockCitationSchema, + }), ]) .nullish(), }) From 627013ec5d82f8dbb888653ad0a8eded3d138fac Mon Sep 17 00:00:00 2001 From: Lewis Wolfe Date: Tue, 28 Oct 2025 16:50:27 +0000 Subject: [PATCH 03/14] change set --- .changeset/wet-geese-remain.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/wet-geese-remain.md diff --git a/.changeset/wet-geese-remain.md b/.changeset/wet-geese-remain.md new file mode 100644 index 000000000000..707c76dd78d1 --- /dev/null +++ b/.changeset/wet-geese-remain.md @@ -0,0 +1,5 @@ +--- +'@ai-sdk/amazon-bedrock': minor +--- + +Fix empty responses when bedrock claude citations object is returned From fa97f4060fa2a8d041e0a40a11002a0eb9b25925 Mon Sep 17 00:00:00 2001 From: Gregor Martynus <39992+gr2m@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:18:13 -0700 Subject: [PATCH 04/14] Apply suggestion from @gr2m --- .changeset/wet-geese-remain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/wet-geese-remain.md b/.changeset/wet-geese-remain.md index 707c76dd78d1..84d54a812121 100644 --- a/.changeset/wet-geese-remain.md +++ b/.changeset/wet-geese-remain.md @@ -1,5 +1,5 @@ --- -'@ai-sdk/amazon-bedrock': minor +'@ai-sdk/amazon-bedrock': patch --- Fix empty responses when bedrock claude citations object is returned From 3a61bad216cadf31777b2ed7b2e6827aeb891d65 Mon Sep 17 00:00:00 2001 From: "vercel-ai-sdk[bot]" <225926702+vercel-ai-sdk[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 17:27:15 +0000 Subject: [PATCH 05/14] style: prettier --- .../amazon-bedrock/src/bedrock-chat-language-model.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts index 1defe1e986d0..75882f199397 100644 --- a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts +++ b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts @@ -351,7 +351,7 @@ export class BedrockChatLanguageModel implements LanguageModelV3 { // provide actual citations in providerMetadata providerMetadata: { bedrock: { - citations: part.citationsContent.citations + citations: part.citationsContent.citations, }, }, }); @@ -816,9 +816,9 @@ const BedrockRedactedReasoningSchema = z.object({ }); const DocumentLocationSchema = z.object({ - documentIndex: z.number(), - start: z.number().min(0), - end: z.number().min(0), + documentIndex: z.number(), + start: z.number().min(0), + end: z.number().min(0), }); const BedrockCitationLocationSchema = z.object({ From 573d7b67de0cb71fc467533ed15408ebeb9787f4 Mon Sep 17 00:00:00 2001 From: Lewis Wolfe Date: Wed, 29 Oct 2025 14:22:55 +0000 Subject: [PATCH 06/14] Update generateText to match anthropic implementation --- .../generate-text/amazon-bedrock-citations.ts | 47 +++++++ .../src/bedrock-chat-language-model.ts | 119 ++++++++++++++++-- 2 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 examples/ai-core/src/generate-text/amazon-bedrock-citations.ts 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..52950b07fef0 --- /dev/null +++ b/examples/ai-core/src/generate-text/amazon-bedrock-citations.ts @@ -0,0 +1,47 @@ +import { bedrock } from '@ai-sdk/amazon-bedrock'; +import { generateText } from 'ai'; +import 'dotenv/config'; + +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 meta = citation.providerMetadata.bedrock; + console.log( + `\n[${i + 1}] "${meta.citedText}" (chars: ${meta.startCharIndex}-${meta.endCharIndex})`, + ); + } + }); +} + +main().catch(console.error); diff --git a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts index 75882f199397..88bd8f4181bf 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,49 @@ 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; + 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.location?.documentPage + ? { + citedText: citation.sourceContent, + startPageNumber: location.start, + endPageNumber: location.end, + } + : { + citedText: citation.sourceContent, + startCharIndex: location.start, + endCharIndex: location.end, + }, + } satisfies SharedV3ProviderMetadata, + }; +} + type BedrockChatConfig = { baseUrl: () => string; headers: Resolvable>; @@ -48,10 +94,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 +352,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 +421,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 @@ -343,19 +439,26 @@ export class BedrockChatLanguageModel implements LanguageModelV3 { // citations if (part.citationsContent) { + // Push the generated content as text for (const generatedContent of part.citationsContent.content) { - // Push the citation generated content (text block) as text to prevent an empty response when citations are present content.push({ type: 'text', text: generatedContent.text, - // provide actual citations in providerMetadata - providerMetadata: { - bedrock: { - citations: part.citationsContent.citations, - }, - }, }); } + + // 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 From 97e861f36aef57700542a6ff570e032c9492eff6 Mon Sep 17 00:00:00 2001 From: Lewis Wolfe Date: Wed, 29 Oct 2025 14:49:15 +0000 Subject: [PATCH 07/14] Allow streaming to handle citations like anthropic --- .../src/bedrock-chat-language-model.ts | 44 ++++++++++++++----- 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts index 88bd8f4181bf..f857ec3222e4 100644 --- a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts +++ b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts @@ -49,7 +49,8 @@ function createCitationSource( }>, generateId: () => string, ): LanguageModelV3Source | undefined { - const location = citation?.location?.documentPage || citation?.location?.documentChar; + const location = + citation?.location?.documentPage || citation?.location?.documentChar; if (!location) { return; } @@ -67,17 +68,16 @@ function createCitationSource( title: citation.title ?? documentInfo.title, filename: documentInfo.filename, providerMetadata: { - bedrock: - citation.location?.documentPage - ? { - citedText: citation.sourceContent, - startPageNumber: location.start, - endPageNumber: location.end, - } - : { - citedText: citation.sourceContent, - startCharIndex: location.start, - endCharIndex: location.end, + bedrock: citation.location?.documentPage + ? { + citedText: citation.sourceContent, + startPageNumber: location.start, + endPageNumber: location.end, + } + : { + citedText: citation.sourceContent, + startCharIndex: location.start, + endCharIndex: location.end, }, } satisfies SharedV3ProviderMetadata, }; @@ -561,6 +561,9 @@ 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 }), @@ -728,6 +731,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]; From 1fee15e073afbdc7bb84a22d18fdf41dfadff6d8 Mon Sep 17 00:00:00 2001 From: Lewis Wolfe Date: Wed, 29 Oct 2025 14:58:06 +0000 Subject: [PATCH 08/14] just send back the whole bedrock citation --- .../src/bedrock-chat-language-model.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts index f857ec3222e4..de2d45396e19 100644 --- a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts +++ b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts @@ -68,17 +68,7 @@ function createCitationSource( title: citation.title ?? documentInfo.title, filename: documentInfo.filename, providerMetadata: { - bedrock: citation.location?.documentPage - ? { - citedText: citation.sourceContent, - startPageNumber: location.start, - endPageNumber: location.end, - } - : { - citedText: citation.sourceContent, - startCharIndex: location.start, - endCharIndex: location.end, - }, + bedrock: citation, } satisfies SharedV3ProviderMetadata, }; } From f5e32c4d187048c30d46ecf54a082e13d2f4417a Mon Sep 17 00:00:00 2001 From: Lewis Wolfe Date: Wed, 29 Oct 2025 15:12:32 +0000 Subject: [PATCH 09/14] fix generateId issue --- packages/amazon-bedrock/src/bedrock-chat-language-model.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts index de2d45396e19..1903f12b767e 100644 --- a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts +++ b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts @@ -553,7 +553,6 @@ export class BedrockChatLanguageModel implements LanguageModelV3 { // 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 }), @@ -587,6 +586,8 @@ export class BedrockChatLanguageModel implements LanguageModelV3 { | { type: 'text' | 'reasoning' } > = {}; + const generateId = this.generateId; + return { stream: response.pipeThrough( new TransformStream< From 4e56ad398d55c0c30105a8aee36482de459e4ea5 Mon Sep 17 00:00:00 2001 From: Lewis Wolfe Date: Wed, 29 Oct 2025 15:49:14 +0000 Subject: [PATCH 10/14] Implement tests for new code --- .../amazon-bedrock/src/bedrock-api-types.ts | 32 + .../src/bedrock-chat-language-model.test.ts | 773 ++++++++++++++++++ 2 files changed, 805 insertions(+) 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..3c121438d6a9 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,149 @@ 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, + pageNumber: 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": { + "location": { + "documentPage": { + "documentIndex": 0, + "pageNumber": 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 +1721,7 @@ describe('doGenerate', () => { | { type: 'tool_use'; id: string; name: string; input: unknown } | BedrockReasoningContentBlock | BedrockRedactedReasoningContentBlock + | BedrockCitationsContentBlock >; toolCalls?: Array<{ id?: string; @@ -2462,6 +2607,634 @@ 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": { + "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": { + "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": { + "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": { + "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": { + "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": { + "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": { + "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({}); From 2bf813047b481b5b32bf04d2cf8c66949dce8a30 Mon Sep 17 00:00:00 2001 From: Lewis Wolfe Date: Wed, 29 Oct 2025 15:50:15 +0000 Subject: [PATCH 11/14] Update examples/ai-core/src/generate-text/amazon-bedrock-citations.ts Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> --- .../ai-core/src/generate-text/amazon-bedrock-citations.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/ai-core/src/generate-text/amazon-bedrock-citations.ts b/examples/ai-core/src/generate-text/amazon-bedrock-citations.ts index 52950b07fef0..34366a4c2f8b 100644 --- a/examples/ai-core/src/generate-text/amazon-bedrock-citations.ts +++ b/examples/ai-core/src/generate-text/amazon-bedrock-citations.ts @@ -37,8 +37,12 @@ async function main() { citation.providerMetadata?.bedrock ) { const meta = citation.providerMetadata.bedrock; + const citedText = meta.sourceContent?.[0]?.text ?? 'N/A'; + const location = meta.location?.documentChar || meta.location?.documentPage; + const startIdx = location?.start ?? 'N/A'; + const endIdx = location?.end ?? 'N/A'; console.log( - `\n[${i + 1}] "${meta.citedText}" (chars: ${meta.startCharIndex}-${meta.endCharIndex})`, + `\n[${i + 1}] "${citedText}" (chars: ${startIdx}-${endIdx})`, ); } }); From b15e5cb7e6d922afffb5065ce6bae1eaac5392be Mon Sep 17 00:00:00 2001 From: Lewis Wolfe Date: Wed, 29 Oct 2025 16:05:46 +0000 Subject: [PATCH 12/14] Update structures --- .../generate-text/amazon-bedrock-citations.ts | 18 ++++++++++++------ .../src/bedrock-chat-language-model.ts | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/examples/ai-core/src/generate-text/amazon-bedrock-citations.ts b/examples/ai-core/src/generate-text/amazon-bedrock-citations.ts index 34366a4c2f8b..455ed5928436 100644 --- a/examples/ai-core/src/generate-text/amazon-bedrock-citations.ts +++ b/examples/ai-core/src/generate-text/amazon-bedrock-citations.ts @@ -1,6 +1,7 @@ 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({ @@ -36,14 +37,19 @@ async function main() { citation.sourceType === 'document' && citation.providerMetadata?.bedrock ) { - const meta = citation.providerMetadata.bedrock; - const citedText = meta.sourceContent?.[0]?.text ?? 'N/A'; - const location = meta.location?.documentChar || meta.location?.documentPage; + 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; const startIdx = location?.start ?? 'N/A'; const endIdx = location?.end ?? 'N/A'; - console.log( - `\n[${i + 1}] "${citedText}" (chars: ${startIdx}-${endIdx})`, - ); + console.log(`\n[${i + 1}] "${citedText}" (chars: ${startIdx}-${endIdx})`); } }); } diff --git a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts index 1903f12b767e..84b6e3bd1a6a 100644 --- a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts +++ b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts @@ -68,7 +68,7 @@ function createCitationSource( title: citation.title ?? documentInfo.title, filename: documentInfo.filename, providerMetadata: { - bedrock: citation, + bedrock: { citation }, } satisfies SharedV3ProviderMetadata, }; } From 33ead4e90e9ebe7b845c2c9f0b7701b92a43a7a0 Mon Sep 17 00:00:00 2001 From: Lewis Wolfe Date: Wed, 29 Oct 2025 16:10:58 +0000 Subject: [PATCH 13/14] Fix anthropic test structure for bedrock --- .../src/bedrock-chat-language-model.test.ts | 194 ++++++++++-------- 1 file changed, 106 insertions(+), 88 deletions(-) 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 3c121438d6a9..b977e8c4a83e 100644 --- a/packages/amazon-bedrock/src/bedrock-chat-language-model.test.ts +++ b/packages/amazon-bedrock/src/bedrock-chat-language-model.test.ts @@ -1419,7 +1419,8 @@ describe('doStream', () => { location: { documentPage: { documentIndex: 0, - pageNumber: 5, + start: 5, + end: 5, }, }, sourceContent: [ @@ -1501,18 +1502,21 @@ describe('doStream', () => { "mediaType": "application/pdf", "providerMetadata": { "bedrock": { - "location": { - "documentPage": { - "documentIndex": 0, - "pageNumber": 5, + "citation": { + "location": { + "documentPage": { + "documentIndex": 0, + "end": 5, + "start": 5, + }, }, + "sourceContent": [ + { + "text": "Revenue increased by 25% year over year", + }, + ], + "title": "Financial Report 2023", }, - "sourceContent": [ - { - "text": "Revenue increased by 25% year over year", - }, - ], - "title": "Financial Report 2023", }, }, "sourceType": "document", @@ -2676,19 +2680,21 @@ describe('doGenerate', () => { "mediaType": "application/pdf", "providerMetadata": { "bedrock": { - "location": { - "documentPage": { - "documentIndex": 0, - "end": 5, - "start": 5, + "citation": { + "location": { + "documentPage": { + "documentIndex": 0, + "end": 5, + "start": 5, + }, }, + "sourceContent": [ + { + "text": "Revenue increased by 25% year over year", + }, + ], + "title": "Financial Report 2023", }, - "sourceContent": [ - { - "text": "Revenue increased by 25% year over year", - }, - ], - "title": "Financial Report 2023", }, }, "sourceType": "document", @@ -2794,19 +2800,21 @@ describe('doGenerate', () => { "mediaType": "application/pdf", "providerMetadata": { "bedrock": { - "location": { - "documentPage": { - "documentIndex": 0, - "end": 3, - "start": 3, + "citation": { + "location": { + "documentPage": { + "documentIndex": 0, + "end": 3, + "start": 3, + }, }, + "sourceContent": [ + { + "text": "Overall revenue growth of 30%", + }, + ], + "title": "Annual Report 2023", }, - "sourceContent": [ - { - "text": "Overall revenue growth of 30%", - }, - ], - "title": "Annual Report 2023", }, }, "sourceType": "document", @@ -2819,19 +2827,21 @@ describe('doGenerate', () => { "mediaType": "application/pdf", "providerMetadata": { "bedrock": { - "location": { - "documentPage": { - "documentIndex": 1, - "end": 1, - "start": 1, + "citation": { + "location": { + "documentPage": { + "documentIndex": 1, + "end": 1, + "start": 1, + }, }, + "sourceContent": [ + { + "text": "Q4 performance exceeded expectations", + }, + ], + "title": "Q4 Quarterly Review", }, - "sourceContent": [ - { - "text": "Q4 performance exceeded expectations", - }, - ], - "title": "Q4 Quarterly Review", }, }, "sourceType": "document", @@ -2911,19 +2921,21 @@ describe('doGenerate', () => { "mediaType": "application/pdf", "providerMetadata": { "bedrock": { - "location": { - "documentChar": { - "documentIndex": 0, - "end": 1345, - "start": 1234, + "citation": { + "location": { + "documentChar": { + "documentIndex": 0, + "end": 1345, + "start": 1234, + }, }, + "sourceContent": [ + { + "text": "Statistical significance was achieved with p < 0.001", + }, + ], + "title": "Research Paper 2023", }, - "sourceContent": [ - { - "text": "Statistical significance was achieved with p < 0.001", - }, - ], - "title": "Research Paper 2023", }, }, "sourceType": "document", @@ -3003,19 +3015,21 @@ describe('doGenerate', () => { "mediaType": "application/pdf", "providerMetadata": { "bedrock": { - "location": { - "documentPage": { - "documentIndex": 0, - "end": 2, - "start": 2, + "citation": { + "location": { + "documentPage": { + "documentIndex": 0, + "end": 2, + "start": 2, + }, }, + "sourceContent": [ + { + "text": "Key findings from the analysis", + }, + ], + "title": null, }, - "sourceContent": [ - { - "text": "Key findings from the analysis", - }, - ], - "title": null, }, }, "sourceType": "document", @@ -3107,19 +3121,21 @@ describe('doGenerate', () => { "mediaType": "application/pdf", "providerMetadata": { "bedrock": { - "location": { - "documentPage": { - "documentIndex": 0, - "end": 7, - "start": 7, + "citation": { + "location": { + "documentPage": { + "documentIndex": 0, + "end": 7, + "start": 7, + }, }, + "sourceContent": [ + { + "text": "Trend analysis reveals upward trajectory", + }, + ], + "title": "Data Analysis Report", }, - "sourceContent": [ - { - "text": "Trend analysis reveals upward trajectory", - }, - ], - "title": "Data Analysis Report", }, }, "sourceType": "document", @@ -3212,19 +3228,21 @@ describe('doGenerate', () => { "mediaType": "application/pdf", "providerMetadata": { "bedrock": { - "location": { - "documentPage": { - "documentIndex": 0, - "end": 1, - "start": 1, + "citation": { + "location": { + "documentPage": { + "documentIndex": 0, + "end": 1, + "start": 1, + }, }, + "sourceContent": [ + { + "text": "Valid source content", + }, + ], + "title": "Valid Citation", }, - "sourceContent": [ - { - "text": "Valid source content", - }, - ], - "title": "Valid Citation", }, }, "sourceType": "document", From eaaf1d1d39c6cf56430821334cc7cfa4d4745e46 Mon Sep 17 00:00:00 2001 From: Lewis Wolfe Date: Wed, 29 Oct 2025 16:20:28 +0000 Subject: [PATCH 14/14] Add chunk option for location --- .../ai-core/src/generate-text/amazon-bedrock-citations.ts | 3 ++- packages/amazon-bedrock/src/bedrock-chat-language-model.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/examples/ai-core/src/generate-text/amazon-bedrock-citations.ts b/examples/ai-core/src/generate-text/amazon-bedrock-citations.ts index 455ed5928436..1461c1d113fa 100644 --- a/examples/ai-core/src/generate-text/amazon-bedrock-citations.ts +++ b/examples/ai-core/src/generate-text/amazon-bedrock-citations.ts @@ -46,7 +46,8 @@ async function main() { const citedText = metaCitation.sourceContent?.[0]?.text ?? 'N/A'; const location = metaCitation.location?.documentChar || - metaCitation.location?.documentPage; + 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})`); diff --git a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts index 84b6e3bd1a6a..8561a8c43118 100644 --- a/packages/amazon-bedrock/src/bedrock-chat-language-model.ts +++ b/packages/amazon-bedrock/src/bedrock-chat-language-model.ts @@ -50,7 +50,9 @@ function createCitationSource( generateId: () => string, ): LanguageModelV3Source | undefined { const location = - citation?.location?.documentPage || citation?.location?.documentChar; + citation?.location?.documentPage || + citation?.location?.documentChar || + citation?.location?.documentChunk; if (!location) { return; }