feat: citation content - #318
Open
cpsievert wants to merge 2 commits into
Open
Conversation
cpsievert
marked this pull request as draft
June 3, 2026 19:02
cpsievert
force-pushed
the
feat/citation-content-model
branch
from
June 12, 2026 16:28
10560f5 to
9a944ca
Compare
cpsievert
commented
Jun 12, 2026
cpsievert
force-pushed
the
feat/citation-content-model
branch
from
June 12, 2026 20:51
e1ef76b to
172147e
Compare
cpsievert
force-pushed
the
feat/citation-content-model
branch
from
July 27, 2026 23:16
3a1eb82 to
079dbb5
Compare
cpsievert
force-pushed
the
feat/citation-content-model
branch
from
July 28, 2026 14:03
757035a to
8a5c24f
Compare
cpsievert
force-pushed
the
feat/citation-content-model
branch
2 times, most recently
from
July 28, 2026 18:39
286c855 to
bcfe6c4
Compare
ContentCitation content
cpsievert
force-pushed
the
feat/citation-content-model
branch
from
July 28, 2026 19:00
1d27ff4 to
fc25cce
Compare
ContentCitation content
cpsievert
force-pushed
the
feat/citation-content-model
branch
from
July 28, 2026 19:16
fc25cce to
4c0ee3a
Compare
cpsievert
force-pushed
the
feat/google-builtin-tool-content
branch
from
July 28, 2026 19:18
c666439 to
8df0ecc
Compare
cpsievert
force-pushed
the
feat/citation-content-model
branch
from
July 28, 2026 19:19
4c0ee3a to
65515bd
Compare
cpsievert
force-pushed
the
feat/google-builtin-tool-content
branch
from
July 28, 2026 19:23
8df0ecc to
79ec639
Compare
cpsievert
force-pushed
the
feat/citation-content-model
branch
2 times, most recently
from
July 28, 2026 19:28
d1f776d to
1fce4b0
Compare
cpsievert
force-pushed
the
feat/citation-content-model
branch
from
July 28, 2026 19:29
1fce4b0 to
c04ff2e
Compare
Providers that support web search now emit ContentCitation content linking a span of the assistant's answer back to the source that grounded it. Citations arrive during streaming as well as on the completed turn. - ContentCitation carries a typed Source (currently WebSource: url + title), the grounded_text it supports, and the provider's cited_text - ContentToolResponseSearch.urls becomes .sources (list[WebSource]), so results keep their titles rather than bare URLs - ContentToolResponseFetch gains status - Google now surfaces web search/fetch requests and results, which it previously dropped
cpsievert
force-pushed
the
feat/citation-content-model
branch
from
July 28, 2026 19:32
c04ff2e to
6872a54
Compare
cpsievert
marked this pull request as ready for review
July 28, 2026 21:40
A Gemini grounding chunk without a web URI (e.g. `retrieved_context`) still grounds a span of the answer, but `google_grounding_citations()` discarded it. Anthropic already emits `source=None` for document citations, so Google was the odd one out, and `ContentCitation.source` documents itself as optional for exactly this case. The old code conflated two reasons a source can be missing. An out-of-bounds `grounding_chunk_indices` entry is still skipped — that one is a provider bug, and inventing a citation from it is worse than dropping it — while a URL-less chunk now yields a citation with no source. Also stop the CHANGELOG from implying that a final-turn citation's position pins it to a span within the accumulated `ContentText`; it doesn't, and `grounded_text` is what does.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
When a model answers using web search or fetch tools, chatlas now surfaces which
sources back the answer and which words each source grounds, normalized across
OpenAI, Anthropic, and Google. Grounded answers carry their citations through to
turns uniformly — both progressively during streaming and on the final turn.
Motivation
Citation information used to be dropped: OpenAI's citation annotations were discarded,
Anthropic's citation deltas never reached normalized content, and Google's grounding
metadata was ignored entirely. Each provider exposed this differently (or not at all),
so there was no portable way to build a UI that shows sources. This branch gives
chatlas one citation model so a downstream consumer (e.g. shinychat) can render
footnote markers and source lists without touching raw provider payloads.
The model is deliberately shaped to be general-ready: web search is the first
consumer, but the same
ContentCitationextends to file / document / RAG citations(uploaded files,
file_search, etc.) by adding newSourcesubclasses — no redesignof
ContentCitationrequired when that work lands.Public API
ContentCitation— a first-class content type representing a source that groundspart of the assistant's answer:
source— the evidence it points to, a typedSource(see below), orNonewhena citation grounds answer text with no resolvable source.
grounded_text— the answer-side span the citation grounds (the words a footnotemarker attaches to / the highlightable span).
cited_text— the source-side evidence quote, when the provider supplies one(populated for
ChatAnthropic()web search today).extra— remaining provider-specific payload.Source— the identity of a piece of evidence. It is a small hierarchy dispatchedon
type; today the one concrete member isWebSource(type="web",url,optional
title). File / document / RAG variants slot in later as additionalsubclasses.
ContentCitation,Source, andWebSourceare all exported fromchatlas.types.Behavior
ContentCitationitems sit in a turn'scontentslist immediately after the textthey ground, in stream order — position and
grounded_texttogether place a citationprecisely.
content="all"), citations arrive interleaved with text(OpenAI/Anthropic) or at stream end (Google). The final turn carries the same items in
the same order, so streaming and replay have one shape.
grounded_textis normalized to one meaning across providers — the resolved answerspan, carried as a string (not offsets), sidestepping Google's byte- vs. OpenAI/
Anthropic's character-offset mismatch.
statusfield.Breaking change
ContentToolResponseSearch.urls(list[str]) is replaced by.sources(
list[WebSource], each carryingurl/title). Migratex.urls→[s.url for s in x.sources]. (This content type is unreleased, so the blast radius iseffectively nil.)
Notes for reviewers
_provider_*.py'sstream_content()and_as_turn(). All three filterContentCitationout of turn serialization (it'sclient-side metadata, not sent back to APIs).
Sourceis a pydantic base class rather than a discriminated union (only one varianttoday). A
create_source(data)factory +@field_validator(mode="before")hooks +SerializeAsAnyon the source-typed fields round-trip the concrete subclass throughmodel_dump()/model_validate(). This upgrades cleanly to a real discriminated uniononce a second variant exists.
unsurfaced.
Test plan
uv run pyright— 0 errors (full package)ruff check+ruff format --check— cleanSource/WebSource, nested-sourceContentCitation,cited_text, round-trip rebuildsWebSource(not a bareSource), exportsContentCitation(withsource.url)in both streaming and final turns; Anthropic additionally asserts
cited_text;all replay VCR cassettes offline
make checkin CI