ADR-013 — Citation extended with cited_text + source_span + document_title¶
Status: Accepted and implemented (2026-04-25).
Context¶
Anthropic’s Citations API returns rich per-citation metadata on every
text block: document_index, cited_text (the exact span the assistant
cited from), start_char_index / end_char_index (offsets into the
source content), and (since 2025) document_title. The pre-bump
AnthropicBackend._flatten_blocks extracted only document_index and
discarded the rest — useful information thrown on the floor.
Two downstream uses matter enough to surface this:
Display. A user reading citeformer output gets
[1]markers and a bibliography. They can’t see which passage in the source the model cited from. The information exists; we just weren’t keeping it.Verification precision.
verify()runs NLI against the entire source content. If we kept the cited span, NLI could score against just that span — much sharper signal, fewer false positives from unrelated passages in the same document.
Other API backends (OpenAI, Gemini, Mistral) and local backends don’t have a notion of “the model cited this exact span” — they emit a source-id integer and that’s it. So the fields must be optional.
Decision¶
Extend
citeformer.core.Citationwith three new optional fields:cited_text: str | None = Nonesource_span: tuple[int, int] | None = Nonedocument_title: str | None = None
Combine with the ADR-012
usagebump — both shape changes land in the sameschema_version: 3. The branch is unreleased; bumping twice for two changes that ship together would be ceremony for no one. TheCitationsnapshot was regenerated to include the new fields with null defaults; pre-existing v2 serialisations deserialise cleanly (the new fields default toNone).The
AnthropicBackendpopulates the metadata via a side-channellast_rich_citations: list[dict]instance attribute — one entry per marker emitted, in the same left-to-right order the orchestrator’s regex parser sees. The_flatten_blockshelper takes an optionalrecord=list parameter that gets appended to as it walks Claude’s citation events.The
Citeformerorchestrator pulls the rich list viagetattr(backend, "last_rich_citations", None)(mirrors thelast_usagepattern from ADR-012) and zips it with the parsed marker list inside_parse_citations. Length-mismatch falls through silently with the new fields leftNone— misaligned data is worse than no data.The
StreamingResult.finalize()path reads the same side-channel, so streaming outputs carry the rich metadata too once the stream exhausts.
Consequences¶
§10.3 contract:
GenerationResult.schema_versionstays at 3 (set by ADR-012). Snapshot regenerated to include the new Citation fields. Pre-bump v2 serialisations deserialise cleanly.AnthropicBackend carries a new
last_rich_citations: list[dict]instance attribute — populated at the end of everygenerate()/ exhaustedstream()call. Empty list before the first call.Other backends are untouched; their
Citationobjects come back withcited_text=None/source_span=None/document_title=None. This is honest signalling (the backend doesn’t know the cited span) rather than a missing capability.Verification doesn’t yet exploit the new fields — that’s a follow-up:
verify()could optionally score NLI againstcited_textinstead ofSource.contentwhen available, sharpening precision on long documents. Out of scope for this PR; tracked as a v0.3 candidate.Anthropic-incompatibility note: per Anthropic’s docs, Citations and Structured Outputs are mutually exclusive — combining them returns a 400. citeformer’s Anthropic backend doesn’t combine them (we use Citations exclusively for this backend), so the constraint doesn’t bite us, but the AnthropicBackend docstring now flags it for users who might add custom system prompts.
Why not extend with a separate CitationAttribution sub-model?¶
Considered: nesting the rich fields under
Citation.attribution: CitationAttribution | None. Rejected because
Most consumers want the cited text directly; a level of nesting hurts ergonomics for the common case.
Pydantic frozen models with optional sub-models complicate the serialisation/round-trip story for downstream tooling.
The three new fields are conceptually one cluster (provider-side span attribution). Flattening keeps
Citationas the single authoritative type per inline marker.