Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c027978
Create citations schema and implement push of text content as default…
lewwolfe Oct 27, 2025
c45e03e
Merge branch 'vercel:main' into fix-bedrock-citations
lewwolfe Oct 27, 2025
c5949b5
fix citation error in bedrock stream schema
lewwolfe Oct 28, 2025
627013e
change set
lewwolfe Oct 28, 2025
f56ff63
Merge branch 'main' into fix-bedrock-citations
lewwolfe Oct 28, 2025
fa97f40
Apply suggestion from @gr2m
gr2m Oct 28, 2025
3a61bad
style: prettier
vercel-ai-sdk[bot] Oct 28, 2025
0782360
Merge branch 'main' into fix-bedrock-citations
lewwolfe Oct 29, 2025
cdfcc9b
Merge branch 'main' into fix-bedrock-citations
lewwolfe Oct 29, 2025
573d7b6
Update generateText to match anthropic implementation
lewwolfe Oct 29, 2025
9bff0ee
Merge branch 'main' into fix-bedrock-citations
lewwolfe Oct 29, 2025
97e861f
Allow streaming to handle citations like anthropic
lewwolfe Oct 29, 2025
1fee15e
just send back the whole bedrock citation
lewwolfe Oct 29, 2025
f5e32c4
fix generateId issue
lewwolfe Oct 29, 2025
4e56ad3
Implement tests for new code
lewwolfe Oct 29, 2025
2bf8130
Update examples/ai-core/src/generate-text/amazon-bedrock-citations.ts
lewwolfe Oct 29, 2025
991dbda
Merge branch 'main' into fix-bedrock-citations
lewwolfe Oct 29, 2025
b15e5cb
Update structures
lewwolfe Oct 29, 2025
33ead4e
Fix anthropic test structure for bedrock
lewwolfe Oct 29, 2025
eaaf1d1
Add chunk option for location
lewwolfe Oct 29, 2025
7a7d4f1
Merge branch 'main' into fix-bedrock-citations
lewwolfe Oct 30, 2025
c214db7
Merge branch 'main' into fix-bedrock-citations
lewwolfe Oct 30, 2025
a33eb7a
Merge branch 'main' into fix-bedrock-citations
lewwolfe Oct 31, 2025
6a7ca94
Merge branch 'main' into fix-bedrock-citations
lewwolfe Nov 3, 2025
62afa64
Merge branch 'main' into fix-bedrock-citations
lewwolfe Nov 4, 2025
e46216e
Merge branch 'main' into fix-bedrock-citations
lewwolfe Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/wet-geese-remain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ai-sdk/amazon-bedrock': patch
---

Fix empty responses when bedrock claude citations object is returned
47 changes: 47 additions & 0 deletions examples/ai-core/src/generate-text/amazon-bedrock-citations.ts
Original file line number Diff line number Diff line change
@@ -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);
169 changes: 168 additions & 1 deletion packages/amazon-bedrock/src/bedrock-chat-language-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
LanguageModelV3Usage,
SharedV3ProviderMetadata,
LanguageModelV3FunctionTool,
LanguageModelV3Prompt,
LanguageModelV3Source,
} from '@ai-sdk/provider';
import {
FetchFunction,
Expand All @@ -17,6 +19,7 @@ import {
combineHeaders,
createJsonErrorResponseHandler,
createJsonResponseHandler,
generateId,
parseProviderOptions,
postJsonToApi,
resolve,
Expand All @@ -37,6 +40,39 @@ 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<typeof BedrockCitationSchema>,
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,
} satisfies SharedV3ProviderMetadata,
};
}

type BedrockChatConfig = {
baseUrl: () => string;
headers: Resolvable<Record<string, string | undefined>>;
Expand All @@ -48,10 +84,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,
Expand Down Expand Up @@ -302,6 +342,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<typeof part, { type: 'file' }>;
return {
title: filePart.filename ?? 'Untitled Document',
filename: filePart.filename,
mediaType: filePart.mediaType,
};
});
}

async doGenerate(
options: Parameters<LanguageModelV3['doGenerate']>[0],
): Promise<Awaited<ReturnType<LanguageModelV3['doGenerate']>>> {
Expand All @@ -328,6 +411,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<LanguageModelV3Content> = [];

// map response content to content array
Expand All @@ -341,6 +427,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) {
Expand Down Expand Up @@ -441,6 +551,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 }),
Expand Down Expand Up @@ -608,6 +721,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];
Expand Down Expand Up @@ -798,6 +928,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({
Expand All @@ -812,6 +975,7 @@ const BedrockResponseSchema = z.object({
z.object({
text: z.string().nullish(),
toolUse: BedrockToolUseSchema.nullish(),
citationsContent: BedrockCitationsContentSchema.nullish(),
reasoningContent: z
.union([
z.object({
Expand Down Expand Up @@ -859,6 +1023,9 @@ const BedrockStreamSchema = z.object({
z.object({
reasoningContent: z.object({ data: z.string() }),
}),
z.object({
citation: BedrockCitationSchema,
}),
])
.nullish(),
})
Expand Down
Loading