Send document citations in message history back to Anthropic and Bedrock
Draft. Don't merge as is. This is the document citation replay taken out of #7752, kept here as the starting point for doing it properly.
With #7752, Anthropic document citations (char_location, which also covers Web Fetch pages) and Bedrock document citations (documentChar) go back to the provider as plain text. This PR sends them back as citations.
Why it was taken out
A document citation names its document only by its position among all the document blocks in the request. When history is trimmed, compacted or edited, documents move. Probed on 2026-10-21/22, Anthropic and Bedrock behave the same way:
- An index past the last document is rejected with
Invalid document index in document citation. - An index that points at a different document is accepted silently.
The code here re-anchors each citation to the document that holds its cited text at its range. It prefers the original index and otherwise takes the only match. That avoids the 400, but it can't tell two documents apart when they hold the same text at the same range, such as templated documents with the same opening. In that case a citation can be sent pointing at the wrong document. A wrong citation is worse than none.
No other framework solves this either:
- Vercel
@ai-sdk/anthropic, langchain-anthropic, langchain-aws and Strands send the index back unchanged. - LibreChat-AI/agents#586 is the resulting 400 after summarising.
- LlamaIndex, Spring AI, LiteLLM and chatlas send plain text.
What it needs before it can merge
Record which document each citation refers to when the response arrives. At that moment the index is exact, because it refers to the request we just sent. Replay then looks the document up instead of guessing:
- When the response arrives: in
AnthropicModelandBedrockConverseModel, for both normal and streamed responses, take the document list of the request we sent. Number it the same way_add_replayed_citationsand_citation_documentsdo: tool-result documents and fetched pages count, tool-call arguments don't. Then record a fingerprint (a hash of the document's text) for each document citation._process_responsedoesn't get the request today, so it needs to be passed in. - Storage: keep the fingerprints next to the raw citations without changing the raw entries. This is a small public decision, such as a new key in
provider_details. - Replay: find the document in the outgoing request with the same fingerprint, and send the citation with that document's index. If there's no such document, leave the citation out. Two documents can only share a fingerprint if their full text is identical, and then either one is the right source.
- Optional: the same identity could fill
DocumentCitationSource.document_id(for example with the document'sidentifier). That would answer "which of my documents was cited" for inline Anthropic documents, which have no title today.
What's already verified and reusable
- Numbering: Anthropic and Bedrock count documents across all messages, including tool results, and Anthropic also counts Web Fetch pages. A failed fetch isn't counted. Offsets are in Python code points.
- Edited text: as in #7752, Anthropic citations only go back while they still cover the whole text part, so edited text and text the Vercel frontend merged go back plain.
- Bedrock: it requires a non-empty title on a replayed citation, but doesn't check the title against the document.
- Recordings: trimmed history, compacted history, fetched pages, a Vercel round trip, tool documents and persisted history on both providers.
- Prompt caching: probed 2026-10-22. History sent the same way every turn keeps the cache. Sending an answer with citations on one turn and without them on the next breaks the cache from that answer onward.