Keep citations in the Vercel AI adapter
Merged
AD
9d ago+603+603+012345678901234567890123456789-28-28-01234567890123456789
The Vercel AI adapter now keeps TextPart.citations when the frontend holds the message history, and lists the cited web pages in the chat UI.
How it works, at a glance
agent run ──► VercelAIEventStream ──► text part + providerMetadata.pydantic_ai.citations ◄── full citations
└─► one source-url part per cited web page ◄── useChat source list
frontend sends messages back ──► load_messages ──► TextPart.citations ──► #7752 sends them to the same provider
| Where | What is sent |
|---|---|
source-url parts | One per cited web page in the message, which useChat and the built-in chat UI show as a source list |
Text part providerMetadata.pydantic_ai | The full citations. When the frontend sends the messages back, they load onto the TextPart and are sent back to the provider that produced them |
Decisions
- Citations go in
providerMetadata, not a custom part. The AI SDK stores that field and sends it back unchanged, so no frontend changes are needed. - Merged text parts load back as one part, with every citation. The adapter already shows consecutive text parts as one UI text part and loads it back as one, keeping the first part's fields. Now every part's citations are kept on it too: ours with their anchors moved onto the merged text, and the provider's raw citations joined. Anthropic web search citations on a merged part aren't sent back, because they no longer cover the whole text (#7752); checked live, the round trip sends plain text and Anthropic accepts it. Recording each part's boundaries to split the text back would have roughly doubled the metadata, for a block structure the providers don't need. Responses without citations look exactly as before.
- OpenAI's raw positions are moved with the text. Anthropic's and Bedrock's raw citations point into the cited document, so merging doesn't affect them (and only Anthropic web search citations are sent back, #7752). OpenAI Responses annotations point into the answer text, so their positions, including file citations'
index, are moved by the length of the text before their part. Two merged OpenAI messages load back under the first message's ID, as they already did before this PR, so the second message's annotations are now sent back under that ID too. Other providers' raw citations aren't sent back, so their positions are left as they are, and the docs say to readcitationsinstead. - The provider name comes from a cited part. Anthropic's uncited parts have none, and replay only sends citations back to the provider that produced them. Raw citations from merged parts of a different provider are dropped.
- Streamed
text-endcarries the first merged part's fields. It now carries the first part'sidandprovider_details, asdump_messagesalready did, rather than the last part's. - Citations that arrive after a part ends aren't streamed. Google and OpenAI Chat return citations at the end of the response (#7434), so when a text part is followed by a tool call, its citations come after its
text-end. They are still in the messagesdump_messagesreturns. - Client-sent metadata is checked. Each citation that doesn't validate or doesn't fit its text is dropped, and the others are kept, so the request never fails because of one. Raw citations are checked again against the request when they're sent back (#7752).
- Citations reach the frontend in full. Excerpts and the provider's citation data in
provider_details['citations']are inproviderMetadata, the same way tool outputs already reach it. The docs say so. - Document sources aren't listed.
source-documentneeds a media type and title that providers don't reliably return. Document citations are still kept in the metadata.source-urlandsource-documentparts sent by the client are ignored on load; citations come only from text-part metadata.
Verification
- Unit round-trip tests, dumped and streamed, on parts shaped like an Anthropic web search answer, a Bedrock document answer and two OpenAI messages: each citation still selects the same text in the merged part, and each OpenAI annotation's moved range still covers its link.
- Tests for invalid citations sent by the client.
- The existing
test_runsnapshot includes the recorded OpenAI citations and theirsource-urlchunks. - Checked by hand in
agent.to_web()with web search: the chat UI lists the cited sources. - Live on 2026-10-23 with Anthropic, OpenAI and Google: streamed citations reach the metadata, each web page is listed once, every citation survives the frontend round trip, and the second turn works.
Checklist
- Any AI generated code has been reviewed line-by-line by the human PR author, who stands by it.
- No breaking changes in accordance with the version policy.
- Any permitted compatibility impact has a label, warning, migration, release note, and exact API-check waiver.
- PR title is fit for the release changelog.