From 4ba0691b033b4bb4f6c19549470a02ee37f9102c Mon Sep 17 00:00:00 2001 From: Carson Date: Fri, 12 Jun 2026 11:25:53 -0500 Subject: [PATCH 1/7] feat(content): add Citation/Source types; update ContentText and ContentToolResponseSearch - Add `Citation` (url, title, cited_text) and `Source` (url, title, fetch_status) types - `ContentText.citations` holds a list of `Citation` objects; merges on `__add__` - `ContentToolResponseSearch.sources` replaces the old `urls` field, adding `fetch_status` - Export `Citation` and `Source` from the public `chatlas.types` namespace --- chatlas/_content.py | 85 ++++++++++++++++++++++++++++---- chatlas/types/__init__.py | 6 +++ tests/test_content.py | 100 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 9 deletions(-) diff --git a/chatlas/_content.py b/chatlas/_content.py index 2243343e..119103e1 100644 --- a/chatlas/_content.py +++ b/chatlas/_content.py @@ -146,6 +146,7 @@ def from_tool(cls, tool: "Tool | ToolBuiltIn") -> "ToolInfo": "web_search_results", "web_fetch_request", "web_fetch_results", + "citation", ] """ A discriminated union of all content types. @@ -170,12 +171,50 @@ def _repr_markdown_(self): return self.__str__() +class Citation(BaseModel): + """ + A source that grounds part of an assistant's answer. + + Produced by built-in web search/fetch tools. + + To render a citation, use ``url``/``title`` (the link) together with + ``cited_text`` (the placement key). ``cited_text`` is a verbatim span of + the assistant's answer that this source grounds; string-match it within the + answer text to position a marker, regardless of provider or streaming order. + It also doubles as the highlightable span shown to the user. + + Parameters + ---------- + url + Link to the cited source. + title + Title of the cited source, when the provider supplies one. + cited_text + A verbatim span of the assistant's answer that this source grounds. + The placement key: a consumer matches it to position a citation marker, + and it doubles as the highlightable span. + """ + + url: str + title: Optional[str] = None + cited_text: Optional[str] = None + + +class Source(BaseModel): + """A page surfaced by a web search (not necessarily cited in the answer).""" + + url: str + title: Optional[str] = None + domain: Optional[str] = None + + class ContentText(Content): """ Text content for a [](`~chatlas.Turn`) """ text: str + citations: list[Citation] = [] content_type: ContentTypeEnum = "text" def __init__(self, **data: Any): @@ -187,7 +226,10 @@ def __init__(self, **data: Any): def __add__(self, other: object) -> "ContentText": if not isinstance(other, ContentText): return NotImplemented # type: ignore[return-value] - return ContentText.model_construct(text=self.text + other.text) + return ContentText.model_construct( + text=self.text + other.text, + citations=[*self.citations, *other.citations], + ) def __str__(self): return self.text @@ -668,9 +710,7 @@ class ContentThinking(Content): @field_serializer("extra") @classmethod - def serialize_extra( - cls, v: Optional[dict[str, Any]] - ) -> Optional[dict[str, Any]]: + def serialize_extra(cls, v: Optional[dict[str, Any]]) -> Optional[dict[str, Any]]: if v is None: return None return serialize_dict_with_bytes(v) @@ -775,20 +815,20 @@ class ContentToolResponseSearch(Content): Parameters ---------- - urls - The URLs returned by the search. + sources + The pages surfaced by the search. extra The raw provider-specific response data. """ - urls: list[str] + sources: list[Source] extra: Optional[dict[str, Any]] = None content_type: ContentTypeEnum = "web_search_results" def __str__(self): - url_list = "\n".join(f"* {url}" for url in self.urls) - return f"[web search results]:\n{url_list}" + lines = "\n".join(f"* {s.url}" for s in self.sources) + return f"[web search results]:\n{lines}" class ContentToolRequestFetch(Content): @@ -826,11 +866,18 @@ class ContentToolResponseFetch(Content): ---------- url The URL that was fetched. + status + A normalized, cross-provider outcome: ``"success"`` if content was + retrieved, ``"error"`` if it was not, or ``None`` when the provider + doesn't report an outcome. Providers expose finer-grained, non-aligned + reasons (e.g. Anthropic's ``url_not_allowed``, Google's ``PAYWALL``); + those are not normalized here but remain available in ``extra``. extra The raw provider-specific response data. """ url: str + status: Optional[Literal["success", "error"]] = None extra: Optional[dict[str, Any]] = None content_type: ContentTypeEnum = "web_fetch_results" @@ -839,6 +886,23 @@ def __str__(self): return f"[web fetch result]: {self.url}" +class ContentCitation(Content): + """ + A citation emitted during streaming (``content="all"``). + + Mirrors the final-turn citation data (which lives on + ``ContentText.citations``). Emitted when a citation becomes known: mid-stream + for Anthropic (applies to the text streamed so far), at turn-end for + OpenAI/Google (``cited_text`` derived from annotation offsets). + """ + + citation: Citation + content_type: ContentTypeEnum = "citation" + + def __str__(self) -> str: + return f"[citation]: {self.citation.url}" + + ContentUnion = Union[ ContentText, ContentImageRemote, @@ -852,6 +916,7 @@ def __str__(self): ContentToolResponseSearch, ContentToolRequestFetch, ContentToolResponseFetch, + ContentCitation, ] @@ -917,6 +982,8 @@ def create_content(data: dict[str, Any]) -> ContentUnion: return ContentToolRequestFetch.model_validate(data) elif ct == "web_fetch_results": return ContentToolResponseFetch.model_validate(data) + elif ct == "citation": + return ContentCitation.model_validate(data) else: raise ValueError(f"Unknown content type: {ct}") diff --git a/chatlas/types/__init__.py b/chatlas/types/__init__.py index 1c7d0b29..73ab3a5b 100644 --- a/chatlas/types/__init__.py +++ b/chatlas/types/__init__.py @@ -4,7 +4,9 @@ SubmitInputArgsT, ) from .._content import ( + Citation, Content, + ContentCitation, ContentImage, ContentImageInline, ContentImageRemote, @@ -20,6 +22,7 @@ ContentToolResponseSearch, ContentToolResult, ImageContentTypes, + Source, ToolAnnotations, ToolInfo, ) @@ -29,7 +32,9 @@ from .._utils import MISSING, MISSING_TYPE __all__ = ( + "Citation", "Content", + "ContentCitation", "ContentImage", "ContentImageInline", "ContentImageRemote", @@ -48,6 +53,7 @@ "ChatResponse", "ChatResponseAsync", "ImageContentTypes", + "Source", "SubmitInputArgsT", "TokenUsage", "ToolAnnotations", diff --git a/tests/test_content.py b/tests/test_content.py index 48688f14..4de69083 100644 --- a/tests/test_content.py +++ b/tests/test_content.py @@ -1,5 +1,13 @@ import pytest from chatlas import ChatOpenAI +from chatlas._content import ( + Citation, + ContentText, + ContentToolResponseFetch, + ContentToolResponseSearch, + Source, + create_content, +) def test_invalid_inputs_give_useful_errors(): @@ -10,3 +18,95 @@ def test_invalid_inputs_give_useful_errors(): with pytest.raises(ValueError): chat.chat(True) # type: ignore + + +def test_citation_defaults(): + c = Citation(url="https://python.org") + assert c.url == "https://python.org" + assert c.title is None + assert c.cited_text is None + + +def test_citation_has_no_offsets(): + c = Citation(url="https://a.com", title="A", cited_text="span") + assert c.cited_text == "span" + assert not hasattr(c, "start_index") + assert not hasattr(c, "end_index") + + +def test_source_defaults(): + s = Source(url="https://python.org") + assert s.url == "https://python.org" + assert s.title is None + assert s.domain is None + + +def test_contenttext_citations_default_empty(): + t = ContentText(text="hello") + assert t.citations == [] + + +def test_contenttext_with_citations(): + t = ContentText( + text="Python 3.14 is the latest release.", + citations=[Citation(url="https://docs.python.org", title="docs", cited_text="Python 3.14")], + ) + assert len(t.citations) == 1 + assert t.citations[0].cited_text == "Python 3.14" + + +def test_search_results_use_sources(): + r = ContentToolResponseSearch( + sources=[Source(url="https://python.org", title="Python", domain="python.org")] + ) + assert r.sources[0].domain == "python.org" + assert "python.org" in str(r) + assert not hasattr(r, "urls") + + +def test_fetch_results_status(): + r = ContentToolResponseFetch(url="https://python.org", status="success") + assert r.status == "success" + assert ContentToolResponseFetch(url="https://x.com").status is None + + +def test_search_results_roundtrip(): + r = ContentToolResponseSearch(sources=[Source(url="https://a.com", title="A")]) + restored = create_content(r.model_dump()) + assert isinstance(restored, ContentToolResponseSearch) + assert restored.sources[0].url == "https://a.com" + + +def test_citation_source_exported_from_types(): + from chatlas.types import Citation, Source # noqa: F401 + import chatlas + # Not re-exported at top level (per design decision) + assert not hasattr(chatlas, "Citation") + + +def test_contenttext_add_merges_citations(): + a = ContentText(text="foo", citations=[Citation(url="https://a.com")]) + b = ContentText(text="bar", citations=[Citation(url="https://b.com")]) + merged = a + b + assert merged.text == "foobar" + assert [c.url for c in merged.citations] == ["https://a.com", "https://b.com"] + + +def test_content_citation_wraps_citation(): + from chatlas._content import Citation, ContentCitation + c = ContentCitation(citation=Citation(url="https://a.com", title="A", cited_text="snippet")) + assert c.citation.url == "https://a.com" + assert c.content_type == "citation" + assert "https://a.com" in str(c) + + +def test_content_citation_roundtrip(): + from chatlas._content import Citation, ContentCitation, create_content + c = ContentCitation(citation=Citation(url="https://a.com", cited_text="foo")) + restored = create_content(c.model_dump()) + assert isinstance(restored, ContentCitation) + assert restored.citation.cited_text == "foo" + + +def test_content_citation_exported_from_types(): + from chatlas.types import ContentCitation # noqa: F401 From bb4dcd4d5fe9fe0af886c51c8e7906ddde9e5444 Mon Sep 17 00:00:00 2001 From: Carson Date: Fri, 12 Jun 2026 11:25:59 -0500 Subject: [PATCH 2/7] refactor(stream): stream_content returns a list; drop stream_text/stream_other_contents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `stream_content()` is now the single hook for streaming — it returns a list of `Content` objects emitted at each chunk. The old per-type hooks (`stream_text`, `stream_other_contents`) are removed; providers and accumulators use the unified list contract instead. `Chat` iterates the returned list to dispatch yields. --- chatlas/_chat.py | 75 +++++++++++++++++++--------------- chatlas/_provider.py | 15 ++----- chatlas/_turn_accumulator.py | 28 +++++++++++-- tests/test_provider.py | 47 +++++++++++++++++++++ tests/test_stream_thinking.py | 4 +- tests/test_turn_accumulator.py | 57 +++++++++++++++++++++++++- 6 files changed, 173 insertions(+), 53 deletions(-) create mode 100644 tests/test_provider.py diff --git a/chatlas/_chat.py b/chatlas/_chat.py index bda891b8..1e6b2152 100644 --- a/chatlas/_chat.py +++ b/chatlas/_chat.py @@ -23,6 +23,7 @@ Optional, Sequence, TypeVar, + Union, cast, overload, ) @@ -32,10 +33,16 @@ from ._callbacks import CallbackManager from ._content import ( Content, + ContentCitation, ContentJson, ContentText, + ContentThinking, ContentThinkingDelta, ContentToolRequest, + ContentToolRequestFetch, + ContentToolRequestSearch, + ContentToolResponseFetch, + ContentToolResponseSearch, ContentToolResult, ToolInfo, ) @@ -95,6 +102,20 @@ class TokensDict(TypedDict): EchoOptions = Literal["output", "all", "none", "text"] +# The values yielded by `.stream()`/`.stream_async()`. Plain text is always +# yielded; the richer content objects only appear when `content="all"`. +StreamedContent = Union[ + str, + ContentThinkingDelta, + ContentToolRequest, + ContentToolResult, + ContentToolRequestSearch, + ContentToolResponseSearch, + ContentToolRequestFetch, + ContentToolResponseFetch, + ContentCitation, +] + T = TypeVar("T") BaseModelT = TypeVar("BaseModelT", bound=BaseModel) @@ -103,6 +124,14 @@ def is_present(value: T | None | MISSING_TYPE) -> TypeGuard[T]: return value is not None and not isinstance(value, MISSING_TYPE) +def _display_text(content: "Content") -> "Optional[str]": + if isinstance(content, ContentText): + return content.text + if isinstance(content, (ContentThinking, ContentThinkingDelta)): + return content.thinking + return None + + class Chat(Generic[SubmitInputArgsT, CompletionT]): """ A chat object that can be used to interact with a language model. @@ -1210,9 +1239,7 @@ def stream( data_model: Optional[type[BaseModel]] = None, kwargs: Optional[SubmitInputArgsT] = None, controller: StreamController | None = None, - ) -> Generator[ - str | ContentThinkingDelta | ContentToolRequest | ContentToolResult, None, None - ]: ... + ) -> Generator[StreamedContent, None, None]: ... def stream( self, @@ -1222,9 +1249,7 @@ def stream( data_model: Optional[type[BaseModel]] = None, kwargs: Optional[SubmitInputArgsT] = None, controller: StreamController | None = None, - ) -> Generator[ - str | ContentThinkingDelta | ContentToolRequest | ContentToolResult, None, None - ]: + ) -> Generator[StreamedContent, None, None]: """ Generate a response from the chat in a streaming fashion. @@ -1298,11 +1323,7 @@ class Person(BaseModel): controller=controller, ) - def wrapper() -> Generator[ - str | ContentThinkingDelta | ContentToolRequest | ContentToolResult, - None, - None, - ]: + def wrapper() -> Generator[StreamedContent, None, None]: with display: for chunk in generator: yield chunk @@ -1329,9 +1350,7 @@ async def stream_async( data_model: Optional[type[BaseModel]] = None, kwargs: Optional[SubmitInputArgsT] = None, controller: StreamController | None = None, - ) -> AsyncGenerator[ - str | ContentThinkingDelta | ContentToolRequest | ContentToolResult, None - ]: ... + ) -> AsyncGenerator[StreamedContent, None]: ... async def stream_async( self, @@ -1341,9 +1360,7 @@ async def stream_async( data_model: Optional[type[BaseModel]] = None, kwargs: Optional[SubmitInputArgsT] = None, controller: StreamController | None = None, - ) -> AsyncGenerator[ - str | ContentThinkingDelta | ContentToolRequest | ContentToolResult, None - ]: + ) -> AsyncGenerator[StreamedContent, None]: """ Generate a response from the chat in a streaming fashion asynchronously. @@ -1422,9 +1439,7 @@ class Person(BaseModel): controller=controller, ) - async def wrapper() -> AsyncGenerator[ - str | ContentThinkingDelta | ContentToolRequest | ContentToolResult, None - ]: + async def wrapper() -> AsyncGenerator[StreamedContent, None]: try: with display: async for chunk in generator: @@ -2586,9 +2601,7 @@ def _chat_impl( data_model: Optional[type[BaseModel]] = None, *, controller: StreamController, - ) -> Generator[ - str | ContentThinkingDelta | ContentToolRequest | ContentToolResult, None, None - ]: ... + ) -> Generator[StreamedContent, None, None]: ... def _chat_impl( self, @@ -2675,9 +2688,7 @@ def _chat_impl_async( data_model: Optional[type[BaseModel]] = None, *, controller: StreamController, - ) -> AsyncGenerator[ - str | ContentThinkingDelta | ContentToolRequest | ContentToolResult, None - ]: ... + ) -> AsyncGenerator[StreamedContent, None]: ... async def _chat_impl_async( self, @@ -2834,11 +2845,9 @@ def emit(text: str | Content): break if controller.cancelled: break - content = self.provider.stream_content(chunk) - if content is not None: - text = self.provider.stream_text(chunk) + for content in self.provider.stream_content(chunk): yield from acc.process_content( - content, text, content_mode, emit + content, _display_text(content), content_mode, emit ) result = self.provider.stream_merge_chunks(result, chunk) @@ -2972,11 +2981,9 @@ def emit(text: str | Content): break if controller.cancelled: break - content = self.provider.stream_content(chunk) - if content is not None: - text = self.provider.stream_text(chunk) + for content in self.provider.stream_content(chunk): for item in acc.process_content( - content, text, content_mode, emit + content, _display_text(content), content_mode, emit ): yield item result = self.provider.stream_merge_chunks(result, chunk) diff --git a/chatlas/_provider.py b/chatlas/_provider.py index 65dbd50c..803fbf57 100644 --- a/chatlas/_provider.py +++ b/chatlas/_provider.py @@ -9,13 +9,14 @@ Iterable, Literal, Optional, + Sequence, TypeVar, overload, ) from pydantic import BaseModel -from ._content import Content, ContentText, ContentThinking +from ._content import Content from ._tools import Tool, ToolBuiltIn from ._turn import AssistantTurn, Turn from ._typing_extensions import NotRequired, TypedDict @@ -230,17 +231,7 @@ async def chat_perform_async( ) -> AsyncIterable[ChatCompletionChunkT] | ChatCompletionT: ... @abstractmethod - def stream_content(self, chunk: ChatCompletionChunkT) -> Optional["Content"]: ... - - def stream_text(self, chunk: ChatCompletionChunkT) -> Optional[str]: - content = self.stream_content(chunk) - if content is None: - return None - if isinstance(content, ContentThinking): - return content.thinking - if isinstance(content, ContentText): - return content.text - return str(content) + def stream_content(self, chunk: ChatCompletionChunkT) -> "Sequence[Content]": ... @abstractmethod def stream_merge_chunks( diff --git a/chatlas/_turn_accumulator.py b/chatlas/_turn_accumulator.py index d558e2ac..d50df222 100644 --- a/chatlas/_turn_accumulator.py +++ b/chatlas/_turn_accumulator.py @@ -4,11 +4,24 @@ from ._content import ( Content, + ContentCitation, ContentThinkingDelta, + ContentToolRequestFetch, + ContentToolRequestSearch, + ContentToolResponseFetch, + ContentToolResponseSearch, ) from ._stream_controller import StreamController from ._turn import AssistantTurn, Turn, UserTurn +STREAMABLE_OTHER = ( + ContentCitation, + ContentToolRequestSearch, + ContentToolResponseSearch, + ContentToolRequestFetch, + ContentToolResponseFetch, +) + class TurnAccumulator: """ @@ -54,9 +67,7 @@ def process_content( items: list[str | Content] = [] if isinstance(content, ContentThinkingDelta) and not self._inside_thinking: - content = ContentThinkingDelta( - thinking=content.thinking, phase="start" - ) + content = ContentThinkingDelta(thinking=content.thinking, phase="start") emit("\n") self._inside_thinking = True elif not isinstance(content, ContentThinkingDelta) and self._inside_thinking: @@ -74,6 +85,9 @@ def process_content( elif text: items.append(text) + if content_mode == "all" and isinstance(content, STREAMABLE_OTHER): + items.append(content) + return items def flush_thinking( @@ -120,7 +134,13 @@ def _update_turn(self, content: Content) -> None: raise RuntimeError("_update_turn called before begin_turn") contents = self._turns[self._turn_idx].contents if contents and type(contents[-1]) is type(content): - merged = contents[-1] + content # type: ignore[operator] + try: + merged = contents[-1] + content # type: ignore[operator] + except TypeError: + # Same-typed but non-mergeable: streamed web/citation content + # (ContentToolRequestSearch, ContentCitation, …) defines no + # __add__, so consecutive items are appended, not merged. + merged = NotImplemented if merged is not NotImplemented: contents[-1] = merged return diff --git a/tests/test_provider.py b/tests/test_provider.py new file mode 100644 index 00000000..030f8211 --- /dev/null +++ b/tests/test_provider.py @@ -0,0 +1,47 @@ +from typing import Any, Optional + +from chatlas._provider import Provider, StandardModelParams, StandardModelParamNames +from chatlas._content import Content +from chatlas._turn import AssistantTurn, Turn +from chatlas._tools import Tool, ToolBuiltIn +from pydantic import BaseModel + + +class _FakeProvider(Provider): + """Minimal stub that implements every abstract method so we can test the concrete ones.""" + + def list_models(self): + return [] + + def chat_perform(self, *, stream, turns, tools, data_model, kwargs): # type: ignore[override] + return None # type: ignore[return-value] + + async def chat_perform_async(self, *, stream, turns, tools, data_model, kwargs): # type: ignore[override] + return None # type: ignore[return-value] + + def stream_content(self, chunk): + return chunk # treat the passed Content as the "chunk" + + def stream_merge_chunks(self, completion, chunk): + return None + + def stream_turn(self, completion, has_data_model): + return AssistantTurn([]) # type: ignore[return-value] + + def value_turn(self, completion, has_data_model): + return AssistantTurn([]) # type: ignore[return-value] + + def value_tokens(self, completion): + return None + + def token_count(self, *args, tools, data_model): + return 0 + + async def token_count_async(self, *args, tools, data_model): + return 0 + + def translate_model_params(self, params: StandardModelParams): + return {} # type: ignore[return-value] + + def supported_model_params(self) -> set[StandardModelParamNames]: + return set() diff --git a/tests/test_stream_thinking.py b/tests/test_stream_thinking.py index 06cc3c86..7de16776 100644 --- a/tests/test_stream_thinking.py +++ b/tests/test_stream_thinking.py @@ -42,8 +42,8 @@ async def _gen(): return _gen() raise NotImplementedError - def stream_content(self, chunk) -> Optional[Content]: - return chunk.content + def stream_content(self, chunk) -> list[Content]: + return [chunk.content] if chunk.content is not None else [] def stream_merge_chunks(self, completion, chunk): return completion or {} diff --git a/tests/test_turn_accumulator.py b/tests/test_turn_accumulator.py index c9d9f77b..e8fe9dbd 100644 --- a/tests/test_turn_accumulator.py +++ b/tests/test_turn_accumulator.py @@ -1,4 +1,11 @@ -from chatlas._content import ContentText +from chatlas._content import ( + Citation, + ContentCitation, + ContentText, + ContentToolRequestSearch, + ContentToolResponseSearch, + Source, +) from chatlas._turn import AssistantTurn, UserTurn from chatlas._turn_accumulator import TurnAccumulator from chatlas._stream_controller import StreamController @@ -27,6 +34,26 @@ def test_update_turn_merges_adjacent_content(): assert turns[1].contents[0].text == "ab" +def test_update_turn_appends_non_mergeable_adjacent_content(): + # Regression: consecutive same-typed content without __add__ (e.g. two + # web-search requests, or multiple citations) must append, not crash. + turns: list = [] + acc = TurnAccumulator(turns, StreamController()) + acc.begin_turn(UserTurn("hello")) + acc._update_turn(ContentToolRequestSearch(query="a")) + acc._update_turn(ContentToolRequestSearch(query="b")) + acc._update_turn(ContentCitation(citation=Citation(url="https://a.com"))) + acc._update_turn(ContentCitation(citation=Citation(url="https://b.com"))) + contents = turns[1].contents + assert len(contents) == 4 + assert [type(c).__name__ for c in contents] == [ + "ContentToolRequestSearch", + "ContentToolRequestSearch", + "ContentCitation", + "ContentCitation", + ] + + def test_complete_turn_replaces_partial(): turns: list = [] controller = StreamController() @@ -85,3 +112,31 @@ def test_finalize_turn_noops_after_complete(): acc.finalize_turn() assert turns[1] is full_turn assert not turns[1].is_partial + + +def _acc() -> TurnAccumulator: + turns: list = [] + acc = TurnAccumulator(turns, StreamController()) + acc.begin_turn(UserTurn("hi")) + return acc + + +def test_process_content_yields_citation_in_all_mode(): + acc = _acc() + cit = ContentCitation(citation=Citation(url="https://a.com")) + out = list(acc.process_content(cit, None, "all", lambda x: None)) + assert out == [cit] + + +def test_process_content_yields_search_results_in_all_mode(): + acc = _acc() + res = ContentToolResponseSearch(sources=[Source(url="https://a.com")]) + out = list(acc.process_content(res, None, "all", lambda x: None)) + assert out == [res] + + +def test_process_content_text_mode_does_not_yield_citation(): + acc = _acc() + cit = ContentCitation(citation=Citation(url="https://a.com")) + out = list(acc.process_content(cit, None, "text", lambda x: None)) + assert out == [] From b114c2ebba2a28c4823890e49b051ba3568a5e38 Mon Sep 17 00:00:00 2001 From: Carson Date: Fri, 12 Jun 2026 11:26:09 -0500 Subject: [PATCH 3/7] feat: surface and stream citations from all providers (OpenAI, Anthropic, Google) Each provider now populates `ContentText.citations` from its web-search results and emits `ContentCitation` items via `stream_content` during streaming: - OpenAI: maps `url_citation` annotations to `Citation`; streams via `annotation.added` - Anthropic: transfers web-search result citations to `ContentText`; streams citations interleaved at `content_block_stop` - Google: surfaces grounding/url-context metadata as `Citation`/`Source`; emits citations at the final chunk via `stream_content` --- chatlas/_provider_anthropic.py | 139 +++++++- chatlas/_provider_google.py | 260 ++++++++++++++- chatlas/_provider_openai.py | 73 ++++- chatlas/_provider_openai_completions.py | 10 +- chatlas/_provider_snowflake.py | 8 +- .../test_anthropic_web_search_streaming.yaml | 287 ++++++++++++++++ .../test_google_web_fetch_streaming.yaml | 80 +++++ .../test_google_web_search_streaming.yaml | 130 ++++++++ .../test_openai_web_search_streaming.yaml | 307 ++++++++++++++++++ tests/conftest.py | 6 +- tests/test_otel.py | 6 +- tests/test_provider_anthropic.py | 56 +++- tests/test_provider_google.py | 110 ++++++- tests/test_provider_openai.py | 47 ++- tests/test_provider_openai_completions.py | 9 +- 15 files changed, 1455 insertions(+), 73 deletions(-) create mode 100644 tests/_vcr/test_provider_anthropic/test_anthropic_web_search_streaming.yaml create mode 100644 tests/_vcr/test_provider_google/test_google_web_fetch_streaming.yaml create mode 100644 tests/_vcr/test_provider_google/test_google_web_search_streaming.yaml create mode 100644 tests/_vcr/test_provider_openai/test_openai_web_search_streaming.yaml diff --git a/chatlas/_provider_anthropic.py b/chatlas/_provider_anthropic.py index b762fefa..0dcd3896 100644 --- a/chatlas/_provider_anthropic.py +++ b/chatlas/_provider_anthropic.py @@ -19,7 +19,9 @@ from ._chat import Chat from ._content import ( + Citation, Content, + ContentCitation, ContentImageInline, ContentImageRemote, ContentJson, @@ -33,6 +35,7 @@ ContentToolResponseFetch, ContentToolResponseSearch, ContentToolResult, + Source, ) from ._logging import log_model_default from ._provider import ( @@ -522,13 +525,102 @@ def _structured_tool_call(**kwargs: Any): return data_model_tool - def stream_content(self, chunk) -> Optional[Content]: + def stream_content(self, chunk) -> list[Content]: if chunk.type == "content_block_delta": if chunk.delta.type == "text_delta": - return ContentText.model_construct(text=chunk.delta.text) + text_block = getattr(self, "_streaming_text_block", None) + if text_block is not None and chunk.index == text_block["index"]: + text_block["text"] += chunk.delta.text + return [ContentText.model_construct(text=chunk.delta.text)] if chunk.delta.type == "thinking_delta": - return ContentThinkingDelta(thinking=chunk.delta.thinking) - return None + return [ContentThinkingDelta(thinking=chunk.delta.thinking)] + if chunk.delta.type == "input_json_delta": + tracked = getattr(self, "_streaming_server_tool_use", None) + if tracked is not None and chunk.index == tracked["index"]: + partial = getattr(chunk.delta, "partial_json", "") + tracked["partial_json"] += partial + if chunk.delta.type == "citations_delta": + text_block = getattr(self, "_streaming_text_block", None) + if text_block is not None and chunk.index == text_block["index"]: + text_block["citations"].append(chunk.delta.citation) + if chunk.type == "content_block_start": + block = chunk.content_block + btype = getattr(block, "type", None) + if btype == "text": + self._streaming_text_block: Optional[dict] = { + "index": chunk.index, + "text": "", + "citations": [], + } + if ( + btype == "server_tool_use" + and getattr(block, "name", None) == "web_search" + ): + self._streaming_server_tool_use: Optional[dict] = { + "index": chunk.index, + "partial_json": "", + } + if btype == "web_search_tool_result": + sources: list[Source] = [] + raw_content = getattr(block, "content", None) + if isinstance(raw_content, list): + for x in raw_content: + sources.append(Source(url=x.url, title=x.title)) + return [ContentToolResponseSearch(sources=sources)] + if btype == "web_fetch_tool_result": + content_fetch = getattr(block, "content", None) + if content_fetch is None: + return [] + url = getattr(content_fetch, "url", "failed") + error_code = getattr(content_fetch, "error_code", None) + # `status` is normalized to success/error; keep the native + # reason (e.g. `url_not_allowed`) in extra, mirroring the turn + # path so streaming and final-turn data don't diverge. + extra = { + "type": btype, + "tool_use_id": getattr(block, "tool_use_id", None), + "content": content_fetch.model_dump(exclude_none=True), + } + return [ + ContentToolResponseFetch( + url=url, + status="error" if error_code else "success", + extra=extra, + ) + ] + if chunk.type == "content_block_stop": + result: list[Content] = [] + text_block = getattr(self, "_streaming_text_block", None) + if text_block is not None and chunk.index == text_block["index"]: + full_text = text_block["text"] + buffered_citations = text_block["citations"] + self._streaming_text_block = None + for c in buffered_citations: + url = getattr(c, "url", None) + if url: + result.append( + ContentCitation( + citation=Citation( + url=url, + title=getattr(c, "title", None), + cited_text=full_text, + ) + ) + ) + tracked = getattr(self, "_streaming_server_tool_use", None) + if tracked is not None and chunk.index == tracked["index"]: + partial_json = tracked["partial_json"] + self._streaming_server_tool_use = None + if partial_json: + try: + parsed = orjson.loads(partial_json) + query = str(parsed.get("query", "")) + if query: + result.append(ContentToolRequestSearch(query=query)) + except orjson.JSONDecodeError: + pass + return result + return [] def stream_merge_chunks(self, completion, chunk): if chunk.type == "message_start": @@ -827,7 +919,27 @@ def _as_turn(self, completion: Message, has_data_model=False) -> AssistantTurn: if uses_new_output_format: contents.append(ContentJson(value=orjson.loads(content.text))) else: - contents.append(ContentText(text=content.text)) + citations = [] + for c in content.citations or []: + # Anthropic citation locations are a union; only + # web_search_result_location carries a `url`. Document- + # grounding location types (char/page/content-block) and + # custom-search-result locations have no url and are skipped. + url = getattr(c, "url", None) + if not url: + continue + # `cited_text` is normalized to the *answer* span (this + # text block), matching OpenAI/Google, so it can be located + # in the reply. (Anthropic's own cited_text is the source + # quote — that lives on the raw block if needed.) + citations.append( + Citation( + url=url, + title=getattr(c, "title", None), + cited_text=content.text, + ) + ) + contents.append(ContentText(text=content.text, citations=citations)) elif content.type == "tool_use": if uses_old_tool_approach and content.name == "_structured_tool_call": if not isinstance(content.input, dict): @@ -894,11 +1006,10 @@ def _as_turn(self, completion: Message, has_data_model=False) -> AssistantTurn: raise ValueError(f"Unknown server tool: {content.name}") elif content.type == "web_search_tool_result": # https://docs.claude.com/en/docs/agents-and-tools/tool-use/web-search-tool#response - urls: list[str] = [] + sources: list[Source] = [] if isinstance(content.content, list): - urls = [x.url for x in content.content] - # Manually construct the extra dict to avoid SDK-internal - # fields (e.g., "caller") that the API doesn't accept + for x in content.content: + sources.append(Source(url=x.url, title=x.title)) extra = { "type": content.type, "tool_use_id": content.tool_use_id, @@ -906,12 +1017,7 @@ def _as_turn(self, completion: Message, has_data_model=False) -> AssistantTurn: if isinstance(content.content, list) else content.content.model_dump(), } - contents.append( - ContentToolResponseSearch( - urls=urls, - extra=extra, - ) - ) + contents.append(ContentToolResponseSearch(sources=sources, extra=extra)) elif content.type == "web_fetch_tool_result": # N.B. type checker thinks this is unreachable due to # ToolUnionParam not including BetaWebFetchTool20250910Param @@ -924,6 +1030,8 @@ def _as_turn(self, completion: Message, has_data_model=False) -> AssistantTurn: # content_fetch is a BetaWebFetchBlock (has .url) or # BetaWebFetchToolResultErrorBlock (error case) url = getattr(content_fetch, "url", "failed") + error_code = getattr(content_fetch, "error_code", None) + status = "error" if error_code else "success" # Manually construct the extra dict to avoid SDK-internal # fields (e.g., "caller") that the API doesn't accept extra = { @@ -934,6 +1042,7 @@ def _as_turn(self, completion: Message, has_data_model=False) -> AssistantTurn: contents.append( ContentToolResponseFetch( url=url, + status=status, extra=extra, ) ) diff --git a/chatlas/_provider_google.py b/chatlas/_provider_google.py index d66bcda2..49907791 100644 --- a/chatlas/_provider_google.py +++ b/chatlas/_provider_google.py @@ -8,7 +8,9 @@ from ._chat import Chat from ._content import ( + Citation, Content, + ContentCitation, ContentImageInline, ContentImageRemote, ContentJson, @@ -17,7 +19,12 @@ ContentThinking, ContentThinkingDelta, ContentToolRequest, + ContentToolRequestFetch, + ContentToolRequestSearch, + ContentToolResponseFetch, + ContentToolResponseSearch, ContentToolResult, + Source, ) from ._logging import log_model_default from ._merge import merge_dicts @@ -33,9 +40,11 @@ GenerateContentConfigDict, GenerateContentResponse, GenerateContentResponseDict, + GroundingMetadataDict, Part, PartDict, ThinkingConfigDict, + UrlContextMetadataDict, ) from .types.google import ChatClientArgs, SubmitInputArgs @@ -392,23 +401,37 @@ def _chat_perform_args( return kwargs_full - def stream_content(self, chunk) -> Optional[Content]: + def stream_content(self, chunk) -> list[Content]: candidates = getattr(chunk, "candidates", None) if not candidates: - return None - content = candidates[0].content - if content is None: - return None - parts = content.parts - if not parts: - return None - part = parts[0] - text = getattr(part, "text", None) - if text is None: - return None - if getattr(part, "thought", None): - return ContentThinkingDelta(thinking=text) - return ContentText.model_construct(text=text) + return [] + candidate = candidates[0] + content = candidate.content + part_contents: list[Content] = [] + if content is not None: + parts = content.parts + if parts: + part = parts[0] + text = getattr(part, "text", None) + if text is not None: + if getattr(part, "thought", None): + part_contents.append(ContentThinkingDelta(thinking=text)) + else: + part_contents.append(ContentText.model_construct(text=text)) + + grounding_metadata = getattr(candidate, "grounding_metadata", None) + url_context_metadata = getattr(candidate, "url_context_metadata", None) + + grounding_contents: list[Content] = [] + if grounding_metadata is not None: + gm_dict = grounding_metadata.model_dump() + if gm_dict.get("grounding_supports"): + grounding_contents.extend(google_grounding_stream_contents(gm_dict)) + if url_context_metadata is not None: + uc_dict = url_context_metadata.model_dump() + grounding_contents.extend(google_url_context_contents(uc_dict)) + + return part_contents + grounding_contents def stream_merge_chunks(self, completion, chunk): chunkd = chunk.model_dump() @@ -508,7 +531,23 @@ def _google_contents(self, turns: list[Turn]) -> list["GoogleContent"]: parts = [self._as_part_type(c) for c in turn.contents] contents.append(GoogleContent(role=turn.role, parts=parts)) elif isinstance(turn, AssistantTurn): - parts = [self._as_part_type(c) for c in turn.contents] + # Grounding/url-context metadata content types are client-side annotations + # extracted from Google's response; they have no corresponding Part to + # send back in the conversation history. + sendable = [ + c + for c in turn.contents + if not isinstance( + c, + ( + ContentToolRequestSearch, + ContentToolResponseSearch, + ContentToolRequestFetch, + ContentToolResponseFetch, + ), + ) + ] + parts = [self._as_part_type(c) for c in sendable] contents.append(GoogleContent(role="model", parts=parts)) else: raise ValueError(f"Unknown role {turn.role}") @@ -582,6 +621,8 @@ def _as_turn( parts: list["PartDict"] = [] finish_reason = None + grounding_metadata = None + url_context_metadata = None for candidate in candidates: content = candidate.get("content") if content: @@ -589,8 +630,15 @@ def _as_turn( finish = candidate.get("finish_reason") if finish: finish_reason = finish + grounding_metadata = grounding_metadata or candidate.get( + "grounding_metadata" + ) + url_context_metadata = url_context_metadata or candidate.get( + "url_context_metadata" + ) contents: list[Content] = [] + text_contents: list[ContentText] = [] for part in parts: text = part.get("text") if text: @@ -599,7 +647,9 @@ def _as_turn( elif part.get("thought"): contents.append(ContentThinking(thinking=text)) else: - contents.append(ContentText(text=text)) + tc = ContentText(text=text) + text_contents.append(tc) + contents.append(tc) function_call = part.get("function_call") if function_call: # Seems name is required but id is optional? @@ -648,6 +698,10 @@ def _as_turn( if isinstance(finish_reason, FinishReason): finish_reason = finish_reason.name + search_contents = google_grounding_contents(grounding_metadata, text_contents) + url_ctx_contents = google_url_context_contents(url_context_metadata) + contents = search_contents + contents + url_ctx_contents + return AssistantTurn( contents, finish_reason=finish_reason, @@ -701,6 +755,178 @@ def supported_model_params(self) -> set[StandardModelParamNames]: } +def google_grounding_contents( + grounding_metadata: "GroundingMetadataDict | None", + text_contents: list[ContentText], +) -> list[Content]: + """Build request/results content and attach citations from Google grounding metadata.""" + if not grounding_metadata: + return [] + + out: list[Content] = [] + + queries = grounding_metadata.get("web_search_queries") or [] + if queries: + out.append( + ContentToolRequestSearch( + query=queries[0], + extra={"web_search_queries": list(queries)}, + ) + ) + + chunks = grounding_metadata.get("grounding_chunks") or [] + # Index-aligned with `chunks` so grounding_chunk_indices resolve correctly. + chunk_sources: list[Source] = [] + for ch in chunks: + web = ch.get("web") or {} + chunk_sources.append( + Source( + url=web.get("uri", "") or "", + title=web.get("title"), + domain=web.get("domain"), + ) + ) + # Only expose sources that have a usable URL (consistent with other providers, + # which never emit an empty-URL Source). + display_sources = [s for s in chunk_sources if s.url] + if display_sources: + out.append( + ContentToolResponseSearch( + sources=display_sources, + extra={"grounding_metadata": grounding_metadata}, + ) + ) + + # NOTE: Google segment offsets are UTF-8 byte offsets. They equal character + # offsets for ASCII but will be off for multi-byte text; treat as approximate + # for non-ASCII. Proper conversion would need text.encode("utf-8")[s:e].decode(). + supports = grounding_metadata.get("grounding_supports") or [] + for sup in supports: + seg = sup.get("segment") or {} + part_index = seg.get("part_index") + if part_index is None: + part_index = 0 + if part_index >= len(text_contents): + continue + target = text_contents[part_index] + # Google provides the grounded span directly as `segment.text`; prefer it + # as the (encoding-proof) cited_text over the byte offsets below. + cited_text = seg.get("text") + for idx in dict.fromkeys(sup.get("grounding_chunk_indices") or []): + if idx >= len(chunk_sources): + continue + src = chunk_sources[idx] + if not src.url: + continue + # NOTE: mutates the shared ContentText already present in `contents`. + target.citations.append( + Citation( + url=src.url, + title=src.title, + cited_text=cited_text, + ) + ) + + return out + + +def google_grounding_stream_contents( + grounding_metadata: "GroundingMetadataDict", +) -> list[Content]: + """Build search request/results + standalone ContentCitations from grounding (streaming). + + Called when a streamed chunk carries grounding_supports. Unlike the non-streaming + path (google_grounding_contents), there are no ContentText objects to attach + citations to, so citations are emitted as standalone ContentCitation items. + """ + out: list[Content] = [] + + queries = grounding_metadata.get("web_search_queries") or [] + if queries: + out.append( + ContentToolRequestSearch( + query=str(queries[0]), + extra={"web_search_queries": list(queries)}, + ) + ) + + chunks = grounding_metadata.get("grounding_chunks") or [] + chunk_sources: list[Source] = [] + for ch in chunks: + web = ch.get("web") or {} + chunk_sources.append( + Source( + url=web.get("uri", "") or "", + title=web.get("title"), + domain=web.get("domain"), + ) + ) + display_sources = [s for s in chunk_sources if s.url] + if display_sources: + out.append( + ContentToolResponseSearch( + sources=display_sources, + extra={"grounding_metadata": grounding_metadata}, + ) + ) + + for sup in grounding_metadata.get("grounding_supports") or []: + seg = sup.get("segment") or {} + cited_text = seg.get("text") + for idx in dict.fromkeys(sup.get("grounding_chunk_indices") or []): + if 0 <= idx < len(chunk_sources) and chunk_sources[idx].url: + src = chunk_sources[idx] + out.append( + ContentCitation( + citation=Citation( + url=src.url, title=src.title, cited_text=cited_text + ) + ) + ) + + return out + + +def google_url_context_contents( + url_context_metadata: "UrlContextMetadataDict | None", +) -> list[Content]: + """Build fetch request/result content from Google url_context_metadata.""" + if not url_context_metadata: + return [] + out: list[Content] = [] + for meta in url_context_metadata.get("url_metadata") or []: + url = meta.get("retrieved_url") + if not url: + continue + out.append(ContentToolRequestFetch(url=url, extra={"url_metadata": meta})) + out.append( + ContentToolResponseFetch( + url=url, + status=normalize_retrieval_status(meta.get("url_retrieval_status")), + # The native status (PAYWALL/UNSAFE/etc.) is preserved in `meta`. + extra={"url_metadata": meta}, + ) + ) + return out + + +def normalize_retrieval_status(raw: object) -> Optional[Literal["success", "error"]]: + """Map Google's UrlRetrievalStatus onto the normalized success/error/None. + + `UNSPECIFIED` carries no outcome, so it maps to `None`; every non-success + status (ERROR/PAYWALL/UNSAFE) collapses to `"error"` (the finer-grained + distinction stays in the content's `extra`). + """ + if raw is None: + return None + value = getattr(raw, "value", raw) + if value == "URL_RETRIEVAL_STATUS_SUCCESS": + return "success" + if value == "URL_RETRIEVAL_STATUS_UNSPECIFIED": + return None + return "error" + + def ChatVertex( *, model: Optional[str] = None, diff --git a/chatlas/_provider_openai.py b/chatlas/_provider_openai.py index 40077444..070b2436 100644 --- a/chatlas/_provider_openai.py +++ b/chatlas/_provider_openai.py @@ -7,11 +7,17 @@ import orjson from openai.types.responses import Response, ResponseStreamEvent +from openai.types.responses.response_function_web_search import ( + ResponseFunctionWebSearch, +) +from openai.types.responses.response_output_text import AnnotationURLCitation from pydantic import BaseModel from ._chat import Chat from ._content import ( + Citation, Content, + ContentCitation, ContentImageInline, ContentImageRemote, ContentJson, @@ -299,21 +305,56 @@ def _chat_perform_args( return kwargs_full - def stream_content(self, chunk) -> Optional[Content]: + def stream_content(self, chunk) -> list[Content]: if chunk.type == "response.output_text.delta": # https://platform.openai.com/docs/api-reference/responses-streaming/response/output_text/delta - return ContentText.model_construct(text=chunk.delta) + self._streaming_text = getattr(self, "_streaming_text", "") + ( + chunk.delta or "" + ) + return [ContentText.model_construct(text=chunk.delta)] + if chunk.type == "response.output_text.annotation.added": + # https://platform.openai.com/docs/api-reference/responses-streaming/response/output_text/annotation_added + # annotation is a plain dict at runtime (SDK types it as `object`) + ann: dict = chunk.annotation # type: ignore[assignment] + if ann.get("type") == "url_citation": + text = getattr(self, "_streaming_text", "") + start = ann.get("start_index") + end = ann.get("end_index") + cited = ( + text[start:end] if (start is not None and end is not None) else None + ) + return [ + ContentCitation( + citation=Citation( + url=ann["url"], + title=ann.get("title"), + cited_text=cited, + ) + ) + ] + return [] + if chunk.type == "response.output_item.done": + item = chunk.item + if isinstance(item, ResponseFunctionWebSearch): + action = item.action + query = getattr(action, "query", None) or None + if not query: + queries = getattr(action, "queries", None) or [] + query = queries[0] if queries else None + if not query: + query = getattr(action, "pattern", None) or None + if not query: + query = getattr(action, "url", None) or "web search" + return [ContentToolRequestSearch(query=query, extra=item.model_dump())] + return [] if chunk.type == "response.reasoning_summary_text.delta": # https://platform.openai.com/docs/api-reference/responses-streaming/response/reasoning_summary_text/delta - return ContentThinkingDelta(thinking=chunk.delta) - if chunk.type == "response.reasoning_summary_text.done": - # The thinking→text transition in _submit_turns already emits - # "\n\n\n" which provides the visual separator. - return None - return None + return [ContentThinkingDelta(thinking=chunk.delta)] + return [] def stream_merge_chunks(self, completion, chunk): if chunk.type == "response.completed": + self._streaming_text = "" return chunk.response elif chunk.type == "response.failed": error = chunk.response.error @@ -388,7 +429,21 @@ def _response_as_turn(completion: Response, has_data_model: bool) -> AssistantTu data = orjson.loads(x.text) contents.append(ContentJson(value=data)) else: - contents.append(ContentText(text=x.text)) + citations = [] + for a in x.annotations or []: + if not isinstance(a, AnnotationURLCitation): + continue + # OpenAI gives offsets but no cited_text; derive the + # grounded span by slicing the block text so every + # provider's citation carries cited_text uniformly. + citations.append( + Citation( + url=a.url, + title=a.title, + cited_text=x.text[a.start_index : a.end_index], + ) + ) + contents.append(ContentText(text=x.text, citations=citations)) elif output.type == "function_call": args = load_tool_request_args(output.arguments, output.name) diff --git a/chatlas/_provider_openai_completions.py b/chatlas/_provider_openai_completions.py index 9dac3bb5..48b99e7f 100644 --- a/chatlas/_provider_openai_completions.py +++ b/chatlas/_provider_openai_completions.py @@ -221,9 +221,9 @@ def _chat_perform_args( return kwargs_full - def stream_content(self, chunk) -> Optional[Content]: + def stream_content(self, chunk) -> list[Content]: if not chunk.choices: - return None + return [] delta = chunk.choices[0].delta # Some OpenAI-compatible providers (e.g. Ollama with qwen3) return @@ -232,12 +232,12 @@ def stream_content(self, chunk) -> Optional[Content]: delta, "reasoning_content", None ) if reasoning is not None: - return ContentThinkingDelta(thinking=reasoning) + return [ContentThinkingDelta(thinking=reasoning)] text = delta.content if text is None: - return None - return ContentText.model_construct(text=text) + return [] + return [ContentText.model_construct(text=text)] def stream_merge_chunks(self, completion, chunk): chunkd = chunk.model_dump() diff --git a/chatlas/_provider_snowflake.py b/chatlas/_provider_snowflake.py index 2127aca8..e07339aa 100644 --- a/chatlas/_provider_snowflake.py +++ b/chatlas/_provider_snowflake.py @@ -356,13 +356,13 @@ def _complete_request( return req - def stream_content(self, chunk) -> Optional[Content]: + def stream_content(self, chunk) -> list[Content]: if not chunk.choices: - return None + return [] delta = chunk.choices[0].delta if delta is None or "content" not in delta: - return None - return ContentText.model_construct(text=delta["content"]) + return [] + return [ContentText.model_construct(text=delta["content"])] # Snowflake sort-of follows OpenAI/Anthropic streaming formats except they # don't have the critical "index" field in the delta that the merge logic diff --git a/tests/_vcr/test_provider_anthropic/test_anthropic_web_search_streaming.yaml b/tests/_vcr/test_provider_anthropic/test_anthropic_web_search_streaming.yaml new file mode 100644 index 00000000..596fb850 --- /dev/null +++ b/tests/_vcr/test_provider_anthropic/test_anthropic_web_search_streaming.yaml @@ -0,0 +1,287 @@ +interactions: +- request: + body: '{"max_tokens": 4096, "messages": [{"role": "user", "content": [{"text": + "When was ggplot2 1.0.0 released to CRAN? Answer in YYYY-MM-DD format.", "type": + "text", "cache_control": {"type": "ephemeral", "ttl": "5m"}}]}], "model": "claude-haiku-4-5-20251001", + "stream": true, "system": [{"type": "text", "text": "[empty string]", "cache_control": + {"type": "ephemeral", "ttl": "5m"}}], "tools": [{"name": "web_search", "type": + "web_search_20250305"}]}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '413' + Content-Type: + - application/json + Host: + - api.anthropic.com + X-Stainless-Async: + - 'false' + anthropic-version: + - '2023-06-01' + x-stainless-read-timeout: + - '600' + x-stainless-timeout: + - NOT_GIVEN + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"model\":\"claude-haiku-4-5-20251001\",\"id\":\"msg_01RvJ82gtJk26N8QKyeQ2cBe\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":2247,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"cache_creation\":{\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_tokens\":26,\"service_tier\":\"standard\"}} + \ }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"server_tool_use\",\"id\":\"srvtoolu_019vghbahbRKPzBwadunDFSW\",\"name\":\"web_search\",\"input\":{}} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"query\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"\\\": + \\\"ggpl\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"ot2 + 1.0.0 \"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"CRAN\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\" + release da\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"te\\\"}\"} + \ }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0 + }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"web_search_tool_result\",\"tool_use_id\":\"srvtoolu_019vghbahbRKPzBwadunDFSW\",\"content\":[{\"type\":\"web_search_result\",\"title\":\"CRAN: + Package ggplot2\",\"url\":\"https://cran.r-project.org/package=ggplot2\",\"encrypted_content\":\"Eo4DCioICxgCIiQ1MzY3Y2M0OC03OTYwLTQ5NmUtOTkyYS1kYmExZTRkMDViNDISDLp+3eWM+kcn3Szl4hoMiek2907qkVbe3bpfIjBn9AYkji4oT7kVBCEyd25CcFLnPj0OaJwMmrEFKvz6cbozX0jrSozBgd2sA9i/wqcqkQJsZ064CjT1PDVT40P6lAK0Pn1if2QhIPFtivkuEc7qn9fQCF6nPhL0cR1JT+YgBB5ArtY5+wFjgMn6WdO0D0N+/rXE9Nks1l8nQX7UQW3VkG4iko8pNVleE8d01uK6wuNXPBefpW/9hn5aBkBwDHuR94ZnuB1umx4RVJ1fLhp8quMJDImyYvA7SfpLeRlKvdzTEdtwj61aDs23IcfRD9TWtqMc9GtMDD8z695huJQxFRGmjW+VkWzDJsZxVnBIuv56r76z+G7RzxlTYQlBBk+Rum7/fCBuY2UqCNielIBn+EZCVnouhIZ2XV+ycL+ELL4pFBfWVbfcY32aommCU8iV1gQhw+twY1o6cDFzwPII4JcYAw==\",\"page_age\":\"November + 14, 2025\"},{\"type\":\"web_search_result\",\"title\":\"Changelog \u2022 ggplot2\",\"url\":\"https://ggplot2.tidyverse.org/news/index.html\",\"encrypted_content\":\"EpAgCioICxgCIiQ1MzY3Y2M0OC03OTYwLTQ5NmUtOTkyYS1kYmExZTRkMDViNDISDO+YKBjlQlpv1H1l/xoM+SRgLmZQM+u5StzRIjB+drkT4j3aeX2Y5KMTXlp+CzGvquNb90WFyW79ROQZI+E+NuNNM78nxWCrMGEOcg8qkx8jOqtqyBcfBCTYLiC4x7soHTUd2plBIck8QWm0L8RVHOVvqs9/qb3DFsZuLG4pCuDZlOn/HIh05WrectN+Xkw7wD0w1oSOgvfHbGsfTbMdMYmI/X4zKbmRZQiiAB+VhIaG2ebh4Ud+MO91ZbDXWYWKw33DySsCUXwwf6u8gXiKnQ5anSRQQGhsmMyLelC/cJfPY0YhNElwE0jiU6uu+4W84OCBU4ZTOrZ+EGScsTzl6puiRf7E4CflWxpoZseLttZFrjplg14XfHXX1Fod4wWVLgNivjxjV34PHkk5ssbnsQu8N/9DErZPl2srpaqR7YFPcHGeZQG49CgFQMMzvHgtKHfzfNmCvBZUe+ReW/mjwtTmMyvxCyux1dFnS0P4Bb4DodrzwgBnlODPN7GmRVHff/9M5qgNctSkrUZ05hkEFan+D7hJHDi9E8DJxluUap2rD9+hJg05LDuZ+dAMZKfdbDsGMBA1e2/2NCW0VBME5+lRgSzYKpGLE0yHlHvIwlgTPKw5UV9st8IzzazrEw6UrHYt9fiWYCWXKHCXM30BEH9eYT4EDSQwbFX/uzqFIZXRsYzcrrOVRDzr8tx40/oE5vZnNm1kQtZS07NRyy0V/Vs3QvquVSDgj0zP+itd5MD9r7EOMTp6LnWAv9nXfEhFDNQxk2b6pBIpJ74u1WSl/QmpXtwf6lhid7Wj8eB8MhGfGiAwJ40Kvwk5iQ3PEZ4B5b9Oh9LBJPvz2Nr+HZoEJBsX950b0nxS5uyOkLMah7iXXkQ7alF+yu4NqpvlyyFkhEiJjdnmryClQivHWHvjHAiDcbWVjTw3V2RCt2xQeKhnn/tXTjIJjyJIUNYhYDCyOToJ1wBlgT2JFDGBPZvuYfGTuk63PhF/DJ8XMkzE9fDBUOEcxjjLDRsXPV/9iP9/jGPGDRko8joXixg7QMzbsg7uX8kPlWhnkcUjfrrUFkiQoBmyV7Ir1B4CIadMVTcVO27K+CXs13pCcStx5MbNVyoJjL7dyJT+UucXmusziq4wdJKsC0Npkg5uEyqSmcN8GBWUyqn09NMNOjKUTeubTZMgElrTLRweiI3920QwdvkxQYB2py+AWr/uMsJOcClKSw4xpiAAV/RmJM/VA3ty4OPyOq2fnXUxjFV/XV/up5n9UByEVeFuENcQgmCC5iTmTXs+DnwaXH6/MY0mb9IKSRcOHyqDhG4uRnpwpGjd1pilw2aBHM8ZbJ9kjyqk06NQ7qtVpTLGPvdp1zxKEcSeVH+aEZrL84iBKVczgWrDpae9aBVr6Kh40eLsEDjubcVZ+39Cwe2z2ldQnYjC4/KkmgP5V4IA240uER8xe5ir96wB6UrQKaSrna9VGFzeaDj3/AnmeD9SjsoVb7hOTFV3sNTDMwbm1f2hyfyiCmYp9MLl9JVZXVKsPIlwI8lJizb2H+36rR1msE+vJTqmetYwHMD+5U+aKXrLkNYHExBUmz6CZnW3DRBQqXRH8VY/5wYD5GU0T6nkPbhDO6AzHy/zBdUYVZNOBlWG8cTDOvSzWQTuL95yI7GAQ5/luxQ5ZfjVyOoY1p82TZ2W8MDojDkXzVWrQ1hT2OPcJYZp14QJOcQkFP8wS9lIBLk88G10Sb3pFs/YNvhxnWAFRhwWZdsCroq0W/kJWsOR3xrrTxzL6GgI6oyYHhxBmPy2H8NOntpTyeyZVdms+6PqJCCWVLNxYQbAYqRznHNH42a6c9m6jlsm7OmBKp06Em8XoHybSvKW2pIYLvKWMPzPBkFIu31ZmXzK7Z/TdmmQkORfYYEbZfknjzOQfEymCx8FRkkDdHLu0wGpnjYmcj1sXdul5640OybCEvAyF2V5pQtheEn1zFesrmhN6b1kDj3wt5T4fWo6+eIgptRzmTVbv5BLT2EbcHqE9qSdBHOtwM9XTC6RRBhg/cGCdNrnM2Hj2IY6xHdqXDlFfFLr0Mhjk70kayjPJi1lir0pehYH1Nt9CjCUj0nG7GI38oyvlnWfzDIRBsjoqrEFnyvuEpxlS7FIApdIVSS360EnCrFtcAc7UbhabACtSkROAiIFmlJg31LeSrhi+jmGG8D2qQC/Wd+IuO/I86GmwqjJ3Gimy3Bj+1sP5CSecCUMC+8f5aeagewBdYZPiWIcfsatJWQK1v6kbnMkc7NTdaoUDuT4Bg1zx1QsLqj0fjzjfm/77rBMjBcHrg0XLVFNaheJr6vGMeKIRgsKzrreydVEKdG6xiXJIpDm5viDp4xLXE1nqCDEeRZLXZHhLzu+eB9m8Y9n4mfEa7gt7Yro1J07LCUiEo0NnTnde+Fxdktzq2cxB3Nudw7iAdL9+vX6fYBn2+8xhrPNvriRGG5uOWV0hUavVXT8cdSMICwDHJcGoyNA1SuEB32qjpSeeY6kZC56z+vbP1NNarb9iQ+787XKNmgKvqPGbyMQywHGPAesm67++VFtKM+dqNFrnO45JbzHdpkdOs87lSGG3ffAlZZgwVsJSKpw0xV3pHKzXyPZHbyOEw9bDT+0jnXtAB3dTC/6vwtjAQ3mqHK8a/EuuHSI3JsZc+kwJDZZNiYdosuHtsI7t5WBA3Ren11b5/sKJM0OYAq6ejrK/Adp9c7jDkpDeRTz0D0O1dMY/wxROGx91uzpnTqnSASsbM0XY2f8Ysywse94Ro4xWN9jUeE6LrbPYEuscReQW+KBwpyMptazUeyVFHGoMtyQR5gXHFSBp6FTHihtCXcsCiAxJoVHFqS+jiUlTD6Ob7T71Df9Fvz9PLPozAAPm09gJRc7lc8RK1ACt+vDOku0hk9B8cmAjKxhaKgw3gaIIDkDu5YMEoBITI4tnIze8hRqJzwj6cc/BXiu0z0VLsveC7/+55nduPJqItXGligDRSHETBSAI/y0P+KS2rFMpUDl1blr5Kec3Dk8unO7oeSNGxoEWEk1rPKOSM1QcQ8E6pR3NFnLBmBAb3HV8oBxyx56RtdSuuAG/hYTtKHy14Aom3560KcFDLyvBC7sgRNM3LzCWhfs0/IYXIqeh0ObwwkapbeuKKqjy3N+S0iqkVLXrE1L41Zm8gczd6LYKFHo5RxIDcaKh0bxLpMQ5RQNrB667k+XxG4Sbsx+pzxYaEVT3teCPBAR1yq7miZjj10GAiMPU5kZqUTA0sExRfM28F1n72fJq8b3jVwoBjbtptmbCNZUKV9+Z8+az7ct2YtK7p0e/2/II87xmvUJbBEmxhHeJzC4IdGFoCHMg818jIOSWGgz8BNDYcwA1bKCNujIRgSh2sJnm+psBvYrZVWqcAemOcBFpvUm29LbgoCYOzg8D2RjSCM2Nn7zI/GrfAh3UNVXVavxJ0jl+Q5R4HeZ+mcUpS31Ur0AWbRjOTUL5YbIJ8lo2+5xXf+RPCIO7PLb7et5GxWS8FpLNrh+R2Qv7COfml2Xb10GKxU30GNtjAr0UNaPQKyWxMQI+t1+d+TIiuhcI6yQHdW5mSEnaI9AwLkbUnziePWKn9pfDKiww7LYG+S0WUnc7xnWFxEJesfAtc1jxM7jlA6e8P80hxo5AiMf/f4CNLfDohsy2BVV58f1u5YE0NDo7bbQlWMtfQ6ZLD38NRAsAxFty/zqSqqjU6oKPcnqAjahfi3PC5oHcbE7K+xOaByE4dNOyrIuZzNaR+T8Dl84nJW+OXTx+06RzZDdw5R7T4dlX4fyGuMW+guX0VTYNHI2IRtDP9hBn2S1mMm7+KSyho2efP3gASQJlc91jKGt7/y3qbSRhv5R04YPeuhmSHk0M2uV7J9+pm0G8CvfGrHFKw4EhCX5W8AwWX7fayjJBw5TFL20fFmwfv6ezlYt0szgFe0LBV+ntLH5jJRboaRW/NUNm6TI8Dd6ahxbajJKv8y68IM27Gf9wrx3mQCVgb0kRV6ywqZeF2THRsx0vVap1CUdSLzPq2SqIR9bigmzhJnHiwJAAlenPxAeJrM5E/toomggvyL0OYqgANxM4tTYc411ULFBiYAJb969S003EAuqoI7ovdm7gxDwU8cmNr89q/6grSS8r/ewbAFn9OvkxjGvEYRQKWlly4mbcid5ChkjKg0A9g8lyKIYHDIV3dzSccRXygutU9kdV4y7hDzXnr2XCTz92k0AlsuehIud/nbsCW85Ufo43CFBHbvDWqVjJaOndgwbRM2zZS1tpxl+QNplPRGz4Q1QDTcYt1pbCT/KJzp/ZXEKJuI5bKiDrQhSXiSawWkuETz/MuJ7y5l4cuno4xexHUTGZkiHjXsL3UjYIC6MRwkWhvnlsOXuUWA32v4p5SlR4F3N9WuIPWts17Vy35lcUC4vDYZCeKuTB83gbOviEF7y43nKMDOwYy0/bGnsyr8T2l4jAh0r+pxNqQConrCmCFHtl1BQztvIhAk/ZnU/KjY4uw7pyaAxrFWIB333Ud3xet0AISAYGfFRA3FpHbxf5NoUI4DVFgUU3lMwrsufwpXs9HdPX6Dy43RxV/qAvCN06aLGPS9kEFBISSW6fYFAnxnmW+pe4/mIgJ0DBEFYQDjGnG+6s1NhByjg7OGDrcMTrgd+S66Dc36kWOwWLFzSFSYBu5Qgm2QDT6VNRwusNQ+6OxQUlYwwWLxpv8gGh8wn64wyFJEnVTw83tH7QMgcVdJObOD+JN92aO3N28z48MYB7E4tidFDEGYNUbhLvFm1LmYmdKEQmMuvssvMZWSkJ96wSqsHm3ECIHN+2oAXylGNF6nl+P33PFnCxC8DlrbBuuuQ8C4jF4kEQWZIHwI386zKRr5fxdbuT0Psf91phhJl80WEFv1RbagkkN4hUTivkNkS2R0cNmEd0Ptq97BDMezwZbo40gGAiD9Vg5FN0mDMvZQLdrztvNr+4tx5zXAvrnaJGNFZ53jfk8u2yPKkeb/7NNuWtEVXM1H48FKaFsJ7ZOXR+4CtCTq1o3T3bRbOQBse69NO7Wbz1qSFKWn13q4HSdrt9wI0cRLOUytOePWpXy2rkB1qX94VbJidf/YdZWwpVtt/X0IrVs5RiSGA5J/muoBD6ZUeTzG9gSyonJC3kCDu8NoOl/Zne1ya/REqFNsbADJoV/NyGjwWcxa4oWGpKaWmpt6SUQwBWe5y3MOzwymxXFcWtkfbOF0Uib1zJ2pzeIsflye4AW3gKpy3LvJSRgGCLLLj//L4MPIuslteh9VppKrf53PH79vs6LxczS7KUVYst4qO8OcXlpaKd17svbT55UPS2pV3T2k6yavKaVVQxTShaNg/omCTAcl/latECDuqBiIo35FTalXCnBg8VbS8CD9alIw+lpD6jHawX0L8RMNRYOexK05DONw9+PqfFusQcSy6gNoYAw==\",\"page_age\":null},{\"type\":\"web_search_result\",\"title\":\"ggplot2 + - Wikipedia\",\"url\":\"https://en.wikipedia.org/wiki/Ggplot2\",\"encrypted_content\":\"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\",\"page_age\":\"4 + days ago\"},{\"type\":\"web_search_result\",\"title\":\"Releases \xB7 tidyverse/ggplot2\",\"url\":\"https://github.com/tidyverse/ggplot2/releases\",\"encrypted_content\":\"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\",\"page_age\":null},{\"type\":\"web_search_result\",\"title\":\"Releases + \xB7 cran/ggplot2\",\"url\":\"https://github.com/cran/ggplot2/releases\",\"encrypted_content\":\"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\",\"page_age\":null},{\"type\":\"web_search_result\",\"title\":\"ggplot2 + R Package Stats, Author, Search and Tutorials | Examples | Downloads | Statistics + | Citations\",\"url\":\"https://rpkg.net/package/ggplot2\",\"encrypted_content\":\"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\",\"page_age\":null},{\"type\":\"web_search_result\",\"title\":\"New + version of ggplot2, new problems \xB7 Issue #255 \xB7 pbiecek/archivist\",\"url\":\"https://github.com/pbiecek/archivist/issues/255\",\"encrypted_content\":\"Ev4KCioICxgCIiQ1MzY3Y2M0OC03OTYwLTQ5NmUtOTkyYS1kYmExZTRkMDViNDISDCiaHqRLxllrMJkz3hoMSDqKwGzjUoXzKzl1IjCoQuAIoZrVNVhpuKjQXTEkb93hWnb4kwCDM/426xVoxK6BpBrZRdU0mHEFJ74wzjMqgQr/vM7kYGFtCQU3IihuFDQ2VaFOYiH1+tlZ6FqL/qkm9hKkHrl+qbNiSZdOp2zNgAb8P3Fu9YNz6LRlgd4flFx2rIYIWKT5L+BzDcxnzQk3vLO4Kd9vlSEOCcaKc6InqQBAHAWlAmN6Y80W9pq9qsHZQAp6JOeglVezXPJ6NG3il0Ylt/N4krMlzCB+KlybzUVF01jw/qGHpasEvcRbE9UHVMqbWSnn//EPO2A432bd7K5JWouMoTlAMHEyLxmoRzPfdjcTc50zT/UpJMgysO2Heobcm4AskLl9qL99jxCIQgx3aTwVWj1k7dQvQsf5ELMty36tou1F+16Clqdr9o6X11Z7N7SRCi6srdDn1wXGA8dJ/DfirhBETZFLGCIT+wxTKDurMOanNHinkViVxoB8nBJbkpfSNF9PwDbKSUaJ5bQDT4VVXuiPCrrhQ+qzqPpN3nY9+vn7AGeuGWURhYoFnUFJFpe2q7MmxmqQT6L2xlLpobH1//kegCnn/gVtDBxng9OKrXSpJj4/HIbDDRXM07McUDcZmm0/UGRaDPQtss2hPgB2hI0rqm9CnKCT/3xjl1/QXzNyXOWkmMyIHjJHxVF0VaoO+YSC+0XiXJErUZG9S9RTJU1zSQ52G5cwQ4Lax8mTb6JMob52Uxjj/rK0xY31y3QwXvon08IpLPBN8IyxbuzWc/iZCPHAISZCGGOS3ZxQrUcEoyv468GY82YM3szpVLXXElV+2E8slfsy/85ek1V0jBaVkhLlSLkB9EntFWAThHJkFFU/paOaTIsR3+ccN63oZmYmmcSIfmHaZgwmzjnNAl+teAfJEILVAuQafu5BALtG9dITzkm78W5A8olL6gGUKfTdHsfHZ6WMnXNjzZd1nO2tAxY4Ti2zDLJfCMnX5WhImYgO+Dbeoaj/xml7naohc9VELeed/yjsW34P8LHJiXcok8Ip/TJxKWO0qWQpDGcRD6ApmQm52DJOLYHN50UGItAPtvqCLc4jumbG9WvYN14cKa8thEhU26qyldupCTgT4Etn85OJEBOE0hTYx0Klx47y/8xrDaN7hofuRcwprRXa9aXSITaXjgbyCuooSVca1qOxGI5uuRN8p2E9n6Wahy4HuwuKAtknoAHJFl95u/bifQk+t2NeWm+QwTld1Frui2TufGN+UrnqG3NGAbHmnEV0n6Jc3EYGyRnHYeHixh6jUeqbf/6BF/gcJkdjX+aWZAMyeVmNi6YqkbtADNhwB2u/VdGYEG8Cf2tuvo5qt9kvNalS94RrD1cN4zfbvk8EZ7bWQGTYFCCCgvYIqw0nexpd2+IK1ymHMV0gfA16r4ibCu2RCuxJJeHvVjp2d7ETqeg5tRR3a4Ecr7/HIGH3qrkfQAi9PLQA7/qFFr9X9aYLnMToza//ER4JVCYVv2CCrs45Dbk0x77ZFtAgv0haJU/twaWzu4tch/3P8igeKBrYxFkoejNNtsYlVKqoskk4Tnm5DnvV5DC+LjiDZpgo7+3+Qh32neEmiUdpJi9PWUVrzp8/l5Twke8XHODsQQcCp9vhmgejE9quqKWV9Jb5zbkkyccxrR5pD2ue/2kmel73K3xwSaI+NYZEq8OmDNe0JBBBnEROkkjtabOJVeh1CgkIBqFaEgDBCpS5SOHCO+Z+n28C3AiHPFApuBCKY7Vb4G9Ql9MrtZ4WvCQZlJc2AuuNwQ4t54KaTNcYAw==\",\"page_age\":null},{\"type\":\"web_search_result\",\"title\":\"Package + 'ggplot2'\",\"url\":\"https://cran.r-project.org/web/packages/ggplot2/ggplot2.pdf\",\"encrypted_content\":\"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\",\"page_age\":null},{\"type\":\"web_search_result\",\"title\":\"NEWS\",\"url\":\"https://cran.r-project.org/web/packages/ggplot2/news/news.html\",\"encrypted_content\":\"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\",\"page_age\":null},{\"type\":\"web_search_result\",\"title\":\"ggpubr: + 'ggplot2' Based Publication Ready Plots - CRAN\",\"url\":\"https://cran.r-project.org/package=ggpubr\",\"encrypted_content\":\"EocKCioICxgCIiQ1MzY3Y2M0OC03OTYwLTQ5NmUtOTkyYS1kYmExZTRkMDViNDISDOkIDZBmEkdkY/ku9hoMgAwxHoCa/5fnY+IPIjD6RCpbsfnCMZsBD7eRZY94juwK0umyC97J959tC9DVYSTkf/a0ISyP/xcfuZzFIX8qigli00/DURjFhLLgUyA1w1cJ5/EVP/UFkZU12WCAYRKV0frD7VPLrzN4gWBYGwGsGOcS984aU16D5JSOgIOOXt3SPABtw2h7fw1FujDCg6pnr8juekw5I4SikzCeLEpigWg+vmc4zfZffsJuoBQaD6jxObaMJhTybDTjWKmMdTz3Eq97FLllTgCXAsErNGokfTY/yxJAo9DxZcZYuMl489OG5ThmbZLe1aQeAZVr3YFfS+XMa+0L885oCoel+S0WIAEOW3xXuzIw9tq+oMKxZvAdYA7usK1WihBWPl/ayTIOuvzL7XFX6SnuSNT7QDgfelRAfl434aSVdr+Y27WitoRNmODJov3zLGjP06pE5cPdeB9Bsvzb6YsHPvSx3FxfBH2+fx2kuCd10HeDo39cpVfYK5zBrbqPUKbECGuv0LajYVT1G7h4PBDyjWEMQJ3SHZCsxJSI+D2V9vDxPDZ5/oLy+zq7TXIkBskDW13JD2pfdRdztXboffddHqE3jIQH/byodnyV/+FPSwVrTaYxwBO7A2dMC/vFpKe1rD5AWdoCbzgqVNgJFjLLEiffSXIIfItrKNf7IwnjMLCVaBaZ7PVJ7Mk5ZiymrhooXB+DHUZabgbjgRFSe0TKju26ZeH+5qw7NLWfIFaWW6ZdfZthayNnIvPhVbdo9gHlb0vtHtLIzo/tV4k2/DV0Efvo8nGLoZ3bRRH3/oeHqGtQB99bTfHLq+1HN2UVsLXlQGDMF0W5b079aoUoCmSM5A8RSdLaovgpzTYjgeutLP7M9KOn9LR0+BTNAj1iDGpRUnKqMqQ1j78aKQmqNs7X64nVT5jOzSaZSwgWyRxTrLz/kcKX2ZUOOH1LjHgfwfbh8OMbGLCkBPOkGoUjhs96qtUN+M9eVK4s9GMDGHNvSFEtlDIfIzo9F9YxM6rPL7cBMIeDBKM1XUsOaueZdgznduuVi89l2EmfPCO8nfxMOgjILjwakdMG6Pvdwm+I2jyH1jOkUrD0z0ihHVSsVVGXWoUvO1yTldIrgVZoSGlZnF8HcmJZ8hqGYhFt78hCOrokBhWU64140ERmowwM1acKif9CBsocpRGHmNdaF8TtOp5Of74fkb3MqLhpLzmK6eCItOl2R1DiDniTMPDvpQXGF7t6RLVFAhj1WM0ZaS2v5sqF3gp1Vjc66rx9lTxPKfsEpTc+XBZ7QtWZ+14G8/TgUOzMVuc7R/y9iZcSchbalagerhaGypAhA8ZJ77UYEi5dMzE5ij8cN8KBIxEDEmG8UUrU+85C2nsXstpVGS27R88vgcR/nhD/ngU6Y0CFd28GogEANt+wpnP5tgf37SJpDDmZCKGOwkHYE16mxsLXWLSOsnuLZTFK4BqN4U9Sjg8Nt+qlFPFnJsn99MpNlGPMObThGAJ5GIu23CsqDcofpmU1SPJivJSQY+NPCh6XmLZElCvBg9DIi7IHCW3c2A0r54n/vdJ/73MoODyk6pfDT6SOsgPVUiFv8FBH8o79eFZE21pfuhiQSxkMr4T/QIs/3LIpuOjYddTSqLs2FaQQJIxVGAM=\",\"page_age\":\"October + 17, 2025\"}]} }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":1 + \ }\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":2,\"content_block\":{\"citations\":[],\"type\":\"text\",\"text\":\"\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"citations_delta\",\"citation\":{\"type\":\"web_search_result_location\",\"cited_text\":\"... + 0.5.1 (2007-06-10 21:49), 0.5.2 ... (2012-09-04 08:16), 0.9.3.1 (2013-03-02 + 15:57), 0.9.3 (2012-12-05 13:11), 1.0.0 (2014-05-21 15:36), 1.0.1 (201...\",\"url\":\"https://rpkg.net/package/ggplot2\",\"title\":\"ggplot2 + R Package Stats, Author, Search and Tutorials | Examples | Downloads | Statistics + | Citations\",\"encrypted_index\":\"Eo8BCioICxgCIiQ1MzY3Y2M0OC03OTYwLTQ5NmUtOTkyYS1kYmExZTRkMDViNDISDBlfY+aB42lx7utL+RoMEXnx0nKjWPvIF3ubIjAS1J+BAkO6BISItYPl35fG04hc7uYO/sKAKtK2rQBix9PM8pk8WhootU0m+gXYDd8qE2Roa5kQDGmdemSY5/GcKiEC6kgYBA==\"}} + }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"text_delta\",\"text\":\"ggplot\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"text_delta\",\"text\":\"2 + 1\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"text_delta\",\"text\":\".0\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"text_delta\",\"text\":\".0 + was\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"text_delta\",\"text\":\" + released on 2014\"} }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"text_delta\",\"text\":\"-05\"}}\n\nevent: + content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":2,\"delta\":{\"type\":\"text_delta\",\"text\":\"-21\"} + \ }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":2}\n\nevent: + content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":3,\"content_block\":{\"type\":\"text\",\"text\":\"\"} + \ }\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":3,\"delta\":{\"type\":\"text_delta\",\"text\":\".\"} + \ }\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":3 + \ }\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"usage\":{\"input_tokens\":2254,\"cache_creation_input_tokens\":17269,\"cache_read_input_tokens\":0,\"output_tokens\":110,\"server_tool_use\":{\"web_search_requests\":1}} + \ }\n\nevent: message_stop\ndata: {\"type\":\"message_stop\" }\n\n" + headers: + CF-RAY: + - 9b7d96fb2803e74d-DEN + Cache-Control: + - no-cache + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Fri, 02 Jan 2026 22:11:01 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Robots-Tag: + - none + anthropic-ratelimit-input-tokens-limit: + - '4000000' + anthropic-ratelimit-input-tokens-remaining: + - '3999000' + anthropic-ratelimit-input-tokens-reset: + - '2026-01-02T22:11:01Z' + anthropic-ratelimit-output-tokens-limit: + - '800000' + anthropic-ratelimit-output-tokens-remaining: + - '800000' + anthropic-ratelimit-output-tokens-reset: + - '2026-01-02T22:11:01Z' + anthropic-ratelimit-requests-limit: + - '4000' + anthropic-ratelimit-requests-remaining: + - '3999' + anthropic-ratelimit-requests-reset: + - '2026-01-02T22:11:01Z' + anthropic-ratelimit-tokens-limit: + - '4800000' + anthropic-ratelimit-tokens-remaining: + - '4799000' + anthropic-ratelimit-tokens-reset: + - '2026-01-02T22:11:01Z' + cf-cache-status: + - DYNAMIC + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '751' + status: + code: 200 + message: OK +- request: + body: '{"max_tokens": 4096, "messages": [{"role": "user", "content": [{"text": + "When was ggplot2 1.0.0 released to CRAN? Answer in YYYY-MM-DD format.", "type": + "text"}]}, {"role": "assistant", "content": [{"type": "server_tool_use", "id": + "srvtoolu_019vghbahbRKPzBwadunDFSW", "name": "web_search", "input": {"query": + "ggplot2 1.0.0 CRAN release date"}}, {"content": [{"encrypted_content": "Eo4DCioICxgCIiQ1MzY3Y2M0OC03OTYwLTQ5NmUtOTkyYS1kYmExZTRkMDViNDISDLp+3eWM+kcn3Szl4hoMiek2907qkVbe3bpfIjBn9AYkji4oT7kVBCEyd25CcFLnPj0OaJwMmrEFKvz6cbozX0jrSozBgd2sA9i/wqcqkQJsZ064CjT1PDVT40P6lAK0Pn1if2QhIPFtivkuEc7qn9fQCF6nPhL0cR1JT+YgBB5ArtY5+wFjgMn6WdO0D0N+/rXE9Nks1l8nQX7UQW3VkG4iko8pNVleE8d01uK6wuNXPBefpW/9hn5aBkBwDHuR94ZnuB1umx4RVJ1fLhp8quMJDImyYvA7SfpLeRlKvdzTEdtwj61aDs23IcfRD9TWtqMc9GtMDD8z695huJQxFRGmjW+VkWzDJsZxVnBIuv56r76z+G7RzxlTYQlBBk+Rum7/fCBuY2UqCNielIBn+EZCVnouhIZ2XV+ycL+ELL4pFBfWVbfcY32aommCU8iV1gQhw+twY1o6cDFzwPII4JcYAw==", + "page_age": "November 14, 2025", "title": "CRAN: Package ggplot2", "type": "web_search_result", + "url": "https://cran.r-project.org/package=ggplot2"}, {"encrypted_content": + "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", + "page_age": null, "title": "Changelog \u2022 ggplot2", "type": "web_search_result", + "url": "https://ggplot2.tidyverse.org/news/index.html"}, {"encrypted_content": + "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", + "page_age": "4 days ago", "title": "ggplot2 - Wikipedia", "type": "web_search_result", + "url": "https://en.wikipedia.org/wiki/Ggplot2"}, {"encrypted_content": "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", + "page_age": null, "title": "Releases \u00b7 tidyverse/ggplot2", "type": "web_search_result", + "url": "https://github.com/tidyverse/ggplot2/releases"}, {"encrypted_content": + "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", + "page_age": null, "title": "Releases \u00b7 cran/ggplot2", "type": "web_search_result", + "url": "https://github.com/cran/ggplot2/releases"}, {"encrypted_content": "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", + "page_age": null, "title": "ggplot2 R Package Stats, Author, Search and Tutorials + | Examples | Downloads | Statistics | Citations", "type": "web_search_result", + "url": "https://rpkg.net/package/ggplot2"}, {"encrypted_content": "Ev4KCioICxgCIiQ1MzY3Y2M0OC03OTYwLTQ5NmUtOTkyYS1kYmExZTRkMDViNDISDCiaHqRLxllrMJkz3hoMSDqKwGzjUoXzKzl1IjCoQuAIoZrVNVhpuKjQXTEkb93hWnb4kwCDM/426xVoxK6BpBrZRdU0mHEFJ74wzjMqgQr/vM7kYGFtCQU3IihuFDQ2VaFOYiH1+tlZ6FqL/qkm9hKkHrl+qbNiSZdOp2zNgAb8P3Fu9YNz6LRlgd4flFx2rIYIWKT5L+BzDcxnzQk3vLO4Kd9vlSEOCcaKc6InqQBAHAWlAmN6Y80W9pq9qsHZQAp6JOeglVezXPJ6NG3il0Ylt/N4krMlzCB+KlybzUVF01jw/qGHpasEvcRbE9UHVMqbWSnn//EPO2A432bd7K5JWouMoTlAMHEyLxmoRzPfdjcTc50zT/UpJMgysO2Heobcm4AskLl9qL99jxCIQgx3aTwVWj1k7dQvQsf5ELMty36tou1F+16Clqdr9o6X11Z7N7SRCi6srdDn1wXGA8dJ/DfirhBETZFLGCIT+wxTKDurMOanNHinkViVxoB8nBJbkpfSNF9PwDbKSUaJ5bQDT4VVXuiPCrrhQ+qzqPpN3nY9+vn7AGeuGWURhYoFnUFJFpe2q7MmxmqQT6L2xlLpobH1//kegCnn/gVtDBxng9OKrXSpJj4/HIbDDRXM07McUDcZmm0/UGRaDPQtss2hPgB2hI0rqm9CnKCT/3xjl1/QXzNyXOWkmMyIHjJHxVF0VaoO+YSC+0XiXJErUZG9S9RTJU1zSQ52G5cwQ4Lax8mTb6JMob52Uxjj/rK0xY31y3QwXvon08IpLPBN8IyxbuzWc/iZCPHAISZCGGOS3ZxQrUcEoyv468GY82YM3szpVLXXElV+2E8slfsy/85ek1V0jBaVkhLlSLkB9EntFWAThHJkFFU/paOaTIsR3+ccN63oZmYmmcSIfmHaZgwmzjnNAl+teAfJEILVAuQafu5BALtG9dITzkm78W5A8olL6gGUKfTdHsfHZ6WMnXNjzZd1nO2tAxY4Ti2zDLJfCMnX5WhImYgO+Dbeoaj/xml7naohc9VELeed/yjsW34P8LHJiXcok8Ip/TJxKWO0qWQpDGcRD6ApmQm52DJOLYHN50UGItAPtvqCLc4jumbG9WvYN14cKa8thEhU26qyldupCTgT4Etn85OJEBOE0hTYx0Klx47y/8xrDaN7hofuRcwprRXa9aXSITaXjgbyCuooSVca1qOxGI5uuRN8p2E9n6Wahy4HuwuKAtknoAHJFl95u/bifQk+t2NeWm+QwTld1Frui2TufGN+UrnqG3NGAbHmnEV0n6Jc3EYGyRnHYeHixh6jUeqbf/6BF/gcJkdjX+aWZAMyeVmNi6YqkbtADNhwB2u/VdGYEG8Cf2tuvo5qt9kvNalS94RrD1cN4zfbvk8EZ7bWQGTYFCCCgvYIqw0nexpd2+IK1ymHMV0gfA16r4ibCu2RCuxJJeHvVjp2d7ETqeg5tRR3a4Ecr7/HIGH3qrkfQAi9PLQA7/qFFr9X9aYLnMToza//ER4JVCYVv2CCrs45Dbk0x77ZFtAgv0haJU/twaWzu4tch/3P8igeKBrYxFkoejNNtsYlVKqoskk4Tnm5DnvV5DC+LjiDZpgo7+3+Qh32neEmiUdpJi9PWUVrzp8/l5Twke8XHODsQQcCp9vhmgejE9quqKWV9Jb5zbkkyccxrR5pD2ue/2kmel73K3xwSaI+NYZEq8OmDNe0JBBBnEROkkjtabOJVeh1CgkIBqFaEgDBCpS5SOHCO+Z+n28C3AiHPFApuBCKY7Vb4G9Ql9MrtZ4WvCQZlJc2AuuNwQ4t54KaTNcYAw==", + "page_age": null, "title": "New version of ggplot2, new problems \u00b7 Issue + #255 \u00b7 pbiecek/archivist", "type": "web_search_result", "url": "https://github.com/pbiecek/archivist/issues/255"}, + {"encrypted_content": "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", + "page_age": null, "title": "Package ''ggplot2''", "type": "web_search_result", + "url": "https://cran.r-project.org/web/packages/ggplot2/ggplot2.pdf"}, {"encrypted_content": + "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", + "page_age": null, "title": "NEWS", "type": "web_search_result", "url": "https://cran.r-project.org/web/packages/ggplot2/news/news.html"}, + {"encrypted_content": "EocKCioICxgCIiQ1MzY3Y2M0OC03OTYwLTQ5NmUtOTkyYS1kYmExZTRkMDViNDISDOkIDZBmEkdkY/ku9hoMgAwxHoCa/5fnY+IPIjD6RCpbsfnCMZsBD7eRZY94juwK0umyC97J959tC9DVYSTkf/a0ISyP/xcfuZzFIX8qigli00/DURjFhLLgUyA1w1cJ5/EVP/UFkZU12WCAYRKV0frD7VPLrzN4gWBYGwGsGOcS984aU16D5JSOgIOOXt3SPABtw2h7fw1FujDCg6pnr8juekw5I4SikzCeLEpigWg+vmc4zfZffsJuoBQaD6jxObaMJhTybDTjWKmMdTz3Eq97FLllTgCXAsErNGokfTY/yxJAo9DxZcZYuMl489OG5ThmbZLe1aQeAZVr3YFfS+XMa+0L885oCoel+S0WIAEOW3xXuzIw9tq+oMKxZvAdYA7usK1WihBWPl/ayTIOuvzL7XFX6SnuSNT7QDgfelRAfl434aSVdr+Y27WitoRNmODJov3zLGjP06pE5cPdeB9Bsvzb6YsHPvSx3FxfBH2+fx2kuCd10HeDo39cpVfYK5zBrbqPUKbECGuv0LajYVT1G7h4PBDyjWEMQJ3SHZCsxJSI+D2V9vDxPDZ5/oLy+zq7TXIkBskDW13JD2pfdRdztXboffddHqE3jIQH/byodnyV/+FPSwVrTaYxwBO7A2dMC/vFpKe1rD5AWdoCbzgqVNgJFjLLEiffSXIIfItrKNf7IwnjMLCVaBaZ7PVJ7Mk5ZiymrhooXB+DHUZabgbjgRFSe0TKju26ZeH+5qw7NLWfIFaWW6ZdfZthayNnIvPhVbdo9gHlb0vtHtLIzo/tV4k2/DV0Efvo8nGLoZ3bRRH3/oeHqGtQB99bTfHLq+1HN2UVsLXlQGDMF0W5b079aoUoCmSM5A8RSdLaovgpzTYjgeutLP7M9KOn9LR0+BTNAj1iDGpRUnKqMqQ1j78aKQmqNs7X64nVT5jOzSaZSwgWyRxTrLz/kcKX2ZUOOH1LjHgfwfbh8OMbGLCkBPOkGoUjhs96qtUN+M9eVK4s9GMDGHNvSFEtlDIfIzo9F9YxM6rPL7cBMIeDBKM1XUsOaueZdgznduuVi89l2EmfPCO8nfxMOgjILjwakdMG6Pvdwm+I2jyH1jOkUrD0z0ihHVSsVVGXWoUvO1yTldIrgVZoSGlZnF8HcmJZ8hqGYhFt78hCOrokBhWU64140ERmowwM1acKif9CBsocpRGHmNdaF8TtOp5Of74fkb3MqLhpLzmK6eCItOl2R1DiDniTMPDvpQXGF7t6RLVFAhj1WM0ZaS2v5sqF3gp1Vjc66rx9lTxPKfsEpTc+XBZ7QtWZ+14G8/TgUOzMVuc7R/y9iZcSchbalagerhaGypAhA8ZJ77UYEi5dMzE5ij8cN8KBIxEDEmG8UUrU+85C2nsXstpVGS27R88vgcR/nhD/ngU6Y0CFd28GogEANt+wpnP5tgf37SJpDDmZCKGOwkHYE16mxsLXWLSOsnuLZTFK4BqN4U9Sjg8Nt+qlFPFnJsn99MpNlGPMObThGAJ5GIu23CsqDcofpmU1SPJivJSQY+NPCh6XmLZElCvBg9DIi7IHCW3c2A0r54n/vdJ/73MoODyk6pfDT6SOsgPVUiFv8FBH8o79eFZE21pfuhiQSxkMr4T/QIs/3LIpuOjYddTSqLs2FaQQJIxVGAM=", + "page_age": "October 17, 2025", "title": "ggpubr: ''ggplot2'' Based Publication + Ready Plots - CRAN", "type": "web_search_result", "url": "https://cran.r-project.org/package=ggpubr"}], + "tool_use_id": "srvtoolu_019vghbahbRKPzBwadunDFSW", "type": "web_search_tool_result"}, + {"text": "ggplot2 1.0.0 was released on 2014-05-21", "type": "text"}, {"text": + ".", "type": "text"}]}, {"role": "user", "content": [{"text": "What month was + that?", "type": "text", "cache_control": {"type": "ephemeral", "ttl": "5m"}}]}], + "model": "claude-haiku-4-5-20251001", "stream": true, "system": [{"type": "text", + "text": "[empty string]", "cache_control": {"type": "ephemeral", "ttl": "5m"}}], + "tools": [{"name": "web_search", "type": "web_search_20250305"}]}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '43677' + Content-Type: + - application/json + Host: + - api.anthropic.com + X-Stainless-Async: + - 'false' + anthropic-version: + - '2023-06-01' + x-stainless-read-timeout: + - '600' + x-stainless-timeout: + - NOT_GIVEN + method: POST + uri: https://api.anthropic.com/v1/messages + response: + body: + string: 'event: message_start + + data: {"type":"message_start","message":{"model":"claude-haiku-4-5-20251001","id":"msg_01TMSeCrqGLutSHQmj8Ls89R","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":37,"cache_read_input_tokens":17269,"cache_creation":{"ephemeral_5m_input_tokens":37,"ephemeral_1h_input_tokens":0},"output_tokens":1,"service_tier":"standard"}} } + + + event: content_block_start + + data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} } + + + event: content_block_delta + + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"That"} } + + + event: content_block_delta + + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" + was May 2"} } + + + event: ping + + data: {"type": "ping"} + + + event: content_block_delta + + data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"014."} } + + + event: content_block_stop + + data: {"type":"content_block_stop","index":0 } + + + event: message_delta + + data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":3,"cache_creation_input_tokens":37,"cache_read_input_tokens":17269,"output_tokens":10} } + + + event: message_stop + + data: {"type":"message_stop" } + + + ' + headers: + CF-RAY: + - 9b7d970fccf94828-DEN + Cache-Control: + - no-cache + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Fri, 02 Jan 2026 22:11:04 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Robots-Tag: + - none + anthropic-ratelimit-input-tokens-limit: + - '4000000' + anthropic-ratelimit-input-tokens-remaining: + - '3999000' + anthropic-ratelimit-input-tokens-reset: + - '2026-01-02T22:11:04Z' + anthropic-ratelimit-output-tokens-limit: + - '800000' + anthropic-ratelimit-output-tokens-remaining: + - '800000' + anthropic-ratelimit-output-tokens-reset: + - '2026-01-02T22:11:04Z' + anthropic-ratelimit-requests-limit: + - '4000' + anthropic-ratelimit-requests-remaining: + - '3999' + anthropic-ratelimit-requests-reset: + - '2026-01-02T22:11:04Z' + anthropic-ratelimit-tokens-limit: + - '4800000' + anthropic-ratelimit-tokens-remaining: + - '4799000' + anthropic-ratelimit-tokens-reset: + - '2026-01-02T22:11:04Z' + cf-cache-status: + - DYNAMIC + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '429' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/_vcr/test_provider_google/test_google_web_fetch_streaming.yaml b/tests/_vcr/test_provider_google/test_google_web_fetch_streaming.yaml new file mode 100644 index 00000000..a3ed05c9 --- /dev/null +++ b/tests/_vcr/test_provider_google/test_google_web_fetch_streaming.yaml @@ -0,0 +1,80 @@ +interactions: +- request: + body: '{"contents": [{"parts": [{"text": "What''s the first movie listed on https://rvest.tidyverse.org/articles/starwars.html?"}], + "role": "user"}], "tools": [{"urlContext": {}}], "generationConfig": {"temperature": + 0}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '212' + Content-Type: + - application/json + Host: + - generativelanguage.googleapis.com + x-goog-api-client: + - google-genai-sdk/1.56.0 gl-python/3.12.11 + method: POST + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse + response: + body: + string: "data: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"The + first movie listed\"}],\"role\": \"model\"},\"index\": 0,\"groundingMetadata\": + {},\"urlContextMetadata\": {\"urlMetadata\": [{\"retrievedUrl\": \"https://rvest.tidyverse.org/articles/starwars.html\",\"urlRetrievalStatus\": + \"URL_RETRIEVAL_STATUS_SUCCESS\"}]}}],\"usageMetadata\": {\"promptTokenCount\": + 25,\"candidatesTokenCount\": 26,\"totalTokenCount\": 1219,\"promptTokensDetails\": + [{\"modality\": \"TEXT\",\"tokenCount\": 25}],\"toolUsePromptTokenCount\": + 1130,\"toolUsePromptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": + 1130}],\"thoughtsTokenCount\": 38},\"modelVersion\": \"gemini-2.5-flash\",\"responseId\": + \"VDVYaY2XOYqM_uMPi8-j8AY\"}\r\n\r\ndata: {\"candidates\": [{\"content\": + {\"parts\": [{\"text\": \" on https://rvest.tidyverse.org/articles/starwars.html + is \\\"\"}],\"role\": \"model\"},\"index\": 0,\"groundingMetadata\": {}}],\"usageMetadata\": + {\"promptTokenCount\": 25,\"candidatesTokenCount\": 44,\"totalTokenCount\": + 1237,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 25}],\"toolUsePromptTokenCount\": + 1130,\"toolUsePromptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": + 1130}],\"thoughtsTokenCount\": 38},\"modelVersion\": \"gemini-2.5-flash\",\"responseId\": + \"VDVYaY2XOYqM_uMPi8-j8AY\"}\r\n\r\ndata: {\"candidates\": [{\"content\": + {\"parts\": [{\"text\": \"The Phantom Menace\\\".\"}],\"role\": \"model\"},\"finishReason\": + \"STOP\",\"index\": 0,\"groundingMetadata\": {\"groundingChunks\": [{\"web\": + {\"uri\": \"https://rvest.tidyverse.org/articles/starwars.html\",\"title\": + \"Star Wars films (static HTML) \u2022 rvest\"}}],\"groundingSupports\": [{\"segment\": + {\"endIndex\": 100,\"text\": \"The first movie listed on https://rvest.tidyverse.org/articles/starwars.html + is \\\"The Phantom Menace\\\"\"},\"groundingChunkIndices\": [0]}]}}],\"usageMetadata\": + {\"promptTokenCount\": 25,\"candidatesTokenCount\": 49,\"totalTokenCount\": + 1242,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 25}],\"toolUsePromptTokenCount\": + 1130,\"toolUsePromptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": + 1130}],\"thoughtsTokenCount\": 38},\"modelVersion\": \"gemini-2.5-flash\",\"responseId\": + \"VDVYaY2XOYqM_uMPi8-j8AY\"}\r\n\r\n" + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Content-Disposition: + - attachment + Content-Type: + - text/event-stream + Date: + - Fri, 02 Jan 2026 21:15:00 GMT + Server: + - scaffolding on HTTPServer2 + Server-Timing: + - gfet4t7; dur=1102 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/_vcr/test_provider_google/test_google_web_search_streaming.yaml b/tests/_vcr/test_provider_google/test_google_web_search_streaming.yaml new file mode 100644 index 00000000..5f669873 --- /dev/null +++ b/tests/_vcr/test_provider_google/test_google_web_search_streaming.yaml @@ -0,0 +1,130 @@ +interactions: +- request: + body: '{"contents": [{"parts": [{"text": "When was ggplot2 1.0.0 released to CRAN? + Answer in YYYY-MM-DD format."}], "role": "user"}], "tools": [{"googleSearch": + {}}], "generationConfig": {"temperature": 0}}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '199' + Content-Type: + - application/json + Host: + - generativelanguage.googleapis.com + x-goog-api-client: + - google-genai-sdk/1.56.0 gl-python/3.12.11 + method: POST + uri: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse + response: + body: + string: "data: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"ggplot2 + version 1.0.0 was released to CRAN on 2014-05-21.\"}],\"role\": \"model\"},\"finishReason\": + \"STOP\",\"index\": 0,\"groundingMetadata\": {\"searchEntryPoint\": {\"renderedContent\": + \"\\u003cstyle\\u003e\\n.container {\\n align-items: center;\\n border-radius: + 8px;\\n display: flex;\\n font-family: Google Sans, Roboto, sans-serif;\\n + \ font-size: 14px;\\n line-height: 20px;\\n padding: 8px 12px;\\n}\\n.chip + {\\n display: inline-block;\\n border: solid 1px;\\n border-radius: 16px;\\n + \ min-width: 14px;\\n padding: 5px 16px;\\n text-align: center;\\n user-select: + none;\\n margin: 0 8px;\\n -webkit-tap-highlight-color: transparent;\\n}\\n.carousel + {\\n overflow: auto;\\n scrollbar-width: none;\\n white-space: nowrap;\\n + \ margin-right: -12px;\\n}\\n.headline {\\n display: flex;\\n margin-right: + 4px;\\n}\\n.gradient-container {\\n position: relative;\\n}\\n.gradient {\\n + \ position: absolute;\\n transform: translate(3px, -9px);\\n height: 36px;\\n + \ width: 9px;\\n}\\n@media (prefers-color-scheme: light) {\\n .container + {\\n background-color: #fafafa;\\n box-shadow: 0 0 0 1px #0000000f;\\n + \ }\\n .headline-label {\\n color: #1f1f1f;\\n }\\n .chip {\\n background-color: + #ffffff;\\n border-color: #d2d2d2;\\n color: #5e5e5e;\\n text-decoration: + none;\\n }\\n .chip:hover {\\n background-color: #f2f2f2;\\n }\\n .chip:focus + {\\n background-color: #f2f2f2;\\n }\\n .chip:active {\\n background-color: + #d8d8d8;\\n border-color: #b6b6b6;\\n }\\n .logo-dark {\\n display: + none;\\n }\\n .gradient {\\n background: linear-gradient(90deg, #fafafa + 15%, #fafafa00 100%);\\n }\\n}\\n@media (prefers-color-scheme: dark) {\\n + \ .container {\\n background-color: #1f1f1f;\\n box-shadow: 0 0 0 1px + #ffffff26;\\n }\\n .headline-label {\\n color: #fff;\\n }\\n .chip + {\\n background-color: #2c2c2c;\\n border-color: #3c4043;\\n color: + #fff;\\n text-decoration: none;\\n }\\n .chip:hover {\\n background-color: + #353536;\\n }\\n .chip:focus {\\n background-color: #353536;\\n }\\n + \ .chip:active {\\n background-color: #464849;\\n border-color: #53575b;\\n + \ }\\n .logo-light {\\n display: none;\\n }\\n .gradient {\\n background: + linear-gradient(90deg, #1f1f1f 15%, #1f1f1f00 100%);\\n }\\n}\\n\\u003c/style\\u003e\\n\\u003cdiv + class=\\\"container\\\"\\u003e\\n \\u003cdiv class=\\\"headline\\\"\\u003e\\n + \ \\u003csvg class=\\\"logo-light\\\" width=\\\"18\\\" height=\\\"18\\\" + viewBox=\\\"9 9 35 35\\\" fill=\\\"none\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\"\\u003e\\n + \ \\u003cpath fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M42.8622 + 27.0064C42.8622 25.7839 42.7525 24.6084 42.5487 23.4799H26.3109V30.1568H35.5897C35.1821 + 32.3041 33.9596 34.1222 32.1258 35.3448V39.6864H37.7213C40.9814 36.677 42.8622 + 32.2571 42.8622 27.0064V27.0064Z\\\" fill=\\\"#4285F4\\\"/\\u003e\\n \\u003cpath + fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M26.3109 43.8555C30.9659 + 43.8555 34.8687 42.3195 37.7213 39.6863L32.1258 35.3447C30.5898 36.3792 28.6306 + 37.0061 26.3109 37.0061C21.8282 37.0061 18.0195 33.9811 16.6559 29.906H10.9194V34.3573C13.7563 + 39.9841 19.5712 43.8555 26.3109 43.8555V43.8555Z\\\" fill=\\\"#34A853\\\"/\\u003e\\n + \ \\u003cpath fill-rule=\\\"evenodd\\\" clip-rule=\\\"evenodd\\\" d=\\\"M16.6559 + 29.8904C16.3111 28.8559 16.1074 27.7588 16.1074 26.6146C16.1074 25.4704 16.3111 + 24.3733 16.6559 23.3388V18.8875H10.9194C9.74388 21.2072 9.06992 23.8247 9.06992 + 26.6146C9.06992 29.4045 9.74388 32.022 10.9194 34.3417L15.3864 30.8621L16.6559 + 29.8904V29.8904Z\\\" fill=\\\"#FBBC05\\\"/\\u003e\\n \\u003cpath fill-rule=\\\"evenodd\\\" + clip-rule=\\\"evenodd\\\" d=\\\"M26.3109 16.2386C28.85 16.2386 31.107 17.1164 + 32.9095 18.8091L37.8466 13.8719C34.853 11.082 30.9659 9.3736 26.3109 9.3736C19.5712 + 9.3736 13.7563 13.245 10.9194 18.8875L16.6559 23.3388C18.0195 19.2636 21.8282 + 16.2386 26.3109 16.2386V16.2386Z\\\" fill=\\\"#EA4335\\\"/\\u003e\\n \\u003c/svg\\u003e\\n + \ \\u003csvg class=\\\"logo-dark\\\" width=\\\"18\\\" height=\\\"18\\\" + viewBox=\\\"0 0 48 48\\\" xmlns=\\\"http://www.w3.org/2000/svg\\\"\\u003e\\n + \ \\u003ccircle cx=\\\"24\\\" cy=\\\"23\\\" fill=\\\"#FFF\\\" r=\\\"22\\\"/\\u003e\\n + \ \\u003cpath d=\\\"M33.76 34.26c2.75-2.56 4.49-6.37 4.49-11.26 0-.89-.08-1.84-.29-3H24.01v5.99h8.03c-.4 + 2.02-1.5 3.56-3.07 4.56v.75l3.91 2.97h.88z\\\" fill=\\\"#4285F4\\\"/\\u003e\\n + \ \\u003cpath d=\\\"M15.58 25.77A8.845 8.845 0 0 0 24 31.86c1.92 0 3.62-.46 + 4.97-1.31l4.79 3.71C31.14 36.7 27.65 38 24 38c-5.93 0-11.01-3.4-13.45-8.36l.17-1.01 + 4.06-2.85h.8z\\\" fill=\\\"#34A853\\\"/\\u003e\\n \\u003cpath d=\\\"M15.59 + 20.21a8.864 8.864 0 0 0 0 5.58l-5.03 3.86c-.98-2-1.53-4.25-1.53-6.64 0-2.39.55-4.64 + 1.53-6.64l1-.22 3.81 2.98.22 1.08z\\\" fill=\\\"#FBBC05\\\"/\\u003e\\n \\u003cpath + d=\\\"M24 14.14c2.11 0 4.02.75 5.52 1.98l4.36-4.36C31.22 9.43 27.81 8 24 8c-5.93 + 0-11.01 3.4-13.45 8.36l5.03 3.85A8.86 8.86 0 0 1 24 14.14z\\\" fill=\\\"#EA4335\\\"/\\u003e\\n + \ \\u003c/svg\\u003e\\n \\u003cdiv class=\\\"gradient-container\\\"\\u003e\\u003cdiv + class=\\\"gradient\\\"\\u003e\\u003c/div\\u003e\\u003c/div\\u003e\\n \\u003c/div\\u003e\\n + \ \\u003cdiv class=\\\"carousel\\\"\\u003e\\n \\u003ca class=\\\"chip\\\" + href=\\\"https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQE75eSdvY972f3gk17-ltnPSn_fT3oGdUZUzeh5BmbuubDdtmayE5D_qhVVfxt0xzDYGEWwSYqTUknHYkxh4KqoXcugsloF1T3brrdsP9Ot6GiEs-lvoavkWlfNmog9vQVe7F9Q33-kDFmnguX11lct2m3s4XASwT-Co5rNXGBMIWCPi60uSyD2zAKBmZA0iLZxlvvY2pSM9UOt\\\"\\u003eggplot2 + release history\\u003c/a\\u003e\\n \\u003ca class=\\\"chip\\\" href=\\\"https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQGSGekQqCaK1aOSrYxAiVrFEu-RzLE0TgQfMsxRhU5kmaBa2UloBVNjXVXFwXOraVNfHhVl0XfJL6W00kqSQAXzMGtMqtKPhEGTwJfe4HYk79nedjYFyL5g55qHO7IIscjDuRm4JYscp4U5QUSRDV0w-ZF8ioTngPjC5_x9fmPCKQOEe_b1504bnqgpLPmuDNboAowG7VGDRC6kazYQxEcNc-A=\\\"\\u003eggplot2 + 1.0.0 CRAN release date\\u003c/a\\u003e\\n \\u003c/div\\u003e\\n\\u003c/div\\u003e\\n\"},\"groundingChunks\": + [{\"web\": {\"uri\": \"https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEjSNbji19KjF_gw6UFj3ZOC3mMjsKRoEV3fUpDEbRUR2lotH6MDj_eipDOvtSDLH_zzb7PzQbAGHzF32W-MKmU9mVYIDBh_qRW37-SyVsMycfRtcqVI0HwZ5qm\",\"title\": + \"rpkg.net\"}}],\"groundingSupports\": [{\"segment\": {\"startIndex\": 20,\"endIndex\": + 57,\"text\": \"0 was released to CRAN on 2014-05-21.\"},\"groundingChunkIndices\": + [0]}],\"webSearchQueries\": [\"ggplot2 1.0.0 CRAN release date\",\"ggplot2 + release history\"]}}],\"usageMetadata\": {\"promptTokenCount\": 27,\"candidatesTokenCount\": + 56,\"totalTokenCount\": 374,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": + 27}],\"toolUsePromptTokenCount\": 118,\"toolUsePromptTokensDetails\": [{\"modality\": + \"TEXT\",\"tokenCount\": 118}],\"thoughtsTokenCount\": 173},\"modelVersion\": + \"gemini-2.5-flash\",\"responseId\": \"UjVYabbAJpvUjMcPirW1qA0\"}\r\n\r\n" + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Content-Disposition: + - attachment + Content-Type: + - text/event-stream + Date: + - Fri, 02 Jan 2026 21:14:58 GMT + Server: + - scaffolding on HTTPServer2 + Server-Timing: + - gfet4t7; dur=2418 + Transfer-Encoding: + - chunked + Vary: + - Origin + - X-Origin + - Referer + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/tests/_vcr/test_provider_openai/test_openai_web_search_streaming.yaml b/tests/_vcr/test_provider_openai/test_openai_web_search_streaming.yaml new file mode 100644 index 00000000..7edb5410 --- /dev/null +++ b/tests/_vcr/test_provider_openai/test_openai_web_search_streaming.yaml @@ -0,0 +1,307 @@ +interactions: +- request: + body: '{"input": [{"role": "user", "content": [{"type": "input_text", "text": + "When was ggplot2 1.0.0 released to CRAN? Answer in YYYY-MM-DD format. The CRAN + archive page has this info."}]}], "model": "gpt-4.1", "store": false, "stream": + true, "tools": [{"type": "web_search"}]}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '255' + Content-Type: + - application/json + Host: + - api.openai.com + X-Stainless-Async: + - 'false' + x-stainless-read-timeout: + - '600' + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: "event: response.created\ndata: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_0296f1f2e122877d0169fb7c5f6ab8819c90a597312ae510fa\",\"object\":\"response\",\"created_at\":1778089055,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4.1-2025-04-14\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"web_search\",\"return_token_budget\":\"default\",\"search_context_size\":\"medium\",\"user_location\":{\"type\":\"approximate\",\"city\":null,\"country\":\"US\",\"region\":null,\"timezone\":null}}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":0}\n\nevent: + response.in_progress\ndata: {\"type\":\"response.in_progress\",\"response\":{\"id\":\"resp_0296f1f2e122877d0169fb7c5f6ab8819c90a597312ae510fa\",\"object\":\"response\",\"created_at\":1778089055,\"status\":\"in_progress\",\"background\":false,\"completed_at\":null,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4.1-2025-04-14\",\"moderation\":null,\"output\":[],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"auto\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"web_search\",\"return_token_budget\":\"default\",\"search_context_size\":\"medium\",\"user_location\":{\"type\":\"approximate\",\"city\":null,\"country\":\"US\",\"region\":null,\"timezone\":null}}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":null,\"user\":null,\"metadata\":{}},\"sequence_number\":1}\n\nevent: + response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"ws_0296f1f2e122877d0169fb7c5fc974819caa17a899327357e1\",\"type\":\"web_search_call\",\"status\":\"in_progress\",\"action\":{\"type\":\"search\"}},\"output_index\":0,\"sequence_number\":2}\n\nevent: + response.web_search_call.in_progress\ndata: {\"type\":\"response.web_search_call.in_progress\",\"item_id\":\"ws_0296f1f2e122877d0169fb7c5fc974819caa17a899327357e1\",\"output_index\":0,\"sequence_number\":3}\n\nevent: + response.web_search_call.searching\ndata: {\"type\":\"response.web_search_call.searching\",\"item_id\":\"ws_0296f1f2e122877d0169fb7c5fc974819caa17a899327357e1\",\"output_index\":0,\"sequence_number\":4}\n\nevent: + response.web_search_call.completed\ndata: {\"type\":\"response.web_search_call.completed\",\"item_id\":\"ws_0296f1f2e122877d0169fb7c5fc974819caa17a899327357e1\",\"output_index\":0,\"sequence_number\":5}\n\nevent: + response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"ws_0296f1f2e122877d0169fb7c5fc974819caa17a899327357e1\",\"type\":\"web_search_call\",\"status\":\"completed\",\"action\":{\"type\":\"search\",\"queries\":[\"ggplot2 + CRAN archive release dates\"],\"query\":\"ggplot2 CRAN archive release dates\"}},\"output_index\":0,\"sequence_number\":6}\n\nevent: + response.output_item.added\ndata: {\"type\":\"response.output_item.added\",\"item\":{\"id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"type\":\"message\",\"status\":\"in_progress\",\"content\":[],\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":7}\n\nevent: + response.content_part.added\ndata: {\"type\":\"response.content_part.added\",\"content_index\":0,\"item_id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[],\"logprobs\":[],\"text\":\"\"},\"sequence_number\":8}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"ggplot2 + version\u202F1.0.0 was\",\"item_id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"logprobs\":[],\"obfuscation\":\"lZtw0ij\",\"output_index\":1,\"sequence_number\":9}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + released on CRAN on **2014\u201105\u201121**.\",\"item_id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"logprobs\":[],\"obfuscation\":\"CUo2EanhTjm3\",\"output_index\":1,\"sequence_number\":10}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + This is confirmed by both the CRAN archive listing and additional package + metadata:\\n\\n-\",\"item_id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"logprobs\":[],\"obfuscation\":\"1AsVEeR0D\",\"output_index\":1,\"sequence_number\":11}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\" + The CRAN archive (Archive/ggplot2) shows the file `ggplot2_1.0.\",\"item_id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"logprobs\":[],\"obfuscation\":\"\",\"output_index\":1,\"sequence_number\":12}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"0.tar.gz` + with a timestamp of **\",\"item_id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"logprobs\":[],\"obfuscation\":\"\",\"output_index\":1,\"sequence_number\":13}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"2014\u201105\u201121 + 15:36**, indicating the date of release \",\"item_id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"logprobs\":[],\"obfuscation\":\"QooOkqiBjHREs\",\"output_index\":1,\"sequence_number\":14}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"([stat.ethz.ch](https://stat.ethz.ch/CRAN/src/contrib/Archive/ggplot2/?utm_source=openai)).\",\"item_id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"logprobs\":[],\"obfuscation\":\"juREQ\",\"output_index\":1,\"sequence_number\":15}\n\nevent: + response.output_text.annotation.added\ndata: {\"type\":\"response.output_text.annotation.added\",\"annotation\":{\"type\":\"url_citation\",\"end_index\":385,\"start_index\":295,\"title\":\"Index + of /CRAN/src/contrib/Archive/ggplot2\",\"url\":\"https://stat.ethz.ch/CRAN/src/contrib/Archive/ggplot2/?utm_source=openai\"},\"annotation_index\":0,\"content_index\":0,\"item_id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"output_index\":1,\"sequence_number\":16}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"\\n- + RDocumentation also lists \u201CLast Published: May 21st, 2014\u201D for version\u202F1.0.\",\"item_id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"logprobs\":[],\"obfuscation\":\"lN\",\"output_index\":1,\"sequence_number\":17}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"0 + \",\"item_id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"logprobs\":[],\"obfuscation\":\"nAC4f8yFUYXMmD\",\"output_index\":1,\"sequence_number\":18}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"([rdocumentation.org](https://www.rdocumentation.org/packages/ggplot2/versions/1.0.0?utm_source=openai)).\\n\\nAnswer + (YYYY\u2011MM\u2011DD): **\",\"item_id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"logprobs\":[],\"obfuscation\":\"6MJCu38LfXEVqn\",\"output_index\":1,\"sequence_number\":19}\n\nevent: + response.output_text.annotation.added\ndata: {\"type\":\"response.output_text.annotation.added\",\"annotation\":{\"type\":\"url_citation\",\"end_index\":570,\"start_index\":466,\"title\":\"ggplot2 + package - RDocumentation\",\"url\":\"https://www.rdocumentation.org/packages/ggplot2/versions/1.0.0?utm_source=openai\"},\"annotation_index\":1,\"content_index\":0,\"item_id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"output_index\":1,\"sequence_number\":20}\n\nevent: + response.output_text.delta\ndata: {\"type\":\"response.output_text.delta\",\"content_index\":0,\"delta\":\"2014\u201105\u201121**.\",\"item_id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"logprobs\":[],\"obfuscation\":\"fdQ\",\"output_index\":1,\"sequence_number\":21}\n\nevent: + response.output_text.done\ndata: {\"type\":\"response.output_text.done\",\"content_index\":0,\"item_id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"logprobs\":[],\"output_index\":1,\"sequence_number\":22,\"text\":\"ggplot2 + version\u202F1.0.0 was released on CRAN on **2014\u201105\u201121**. This + is confirmed by both the CRAN archive listing and additional package metadata:\\n\\n- + The CRAN archive (Archive/ggplot2) shows the file `ggplot2_1.0.0.tar.gz` with + a timestamp of **2014\u201105\u201121 15:36**, indicating the date of release + ([stat.ethz.ch](https://stat.ethz.ch/CRAN/src/contrib/Archive/ggplot2/?utm_source=openai)).\\n- + RDocumentation also lists \u201CLast Published: May 21st, 2014\u201D for version\u202F1.0.0 + ([rdocumentation.org](https://www.rdocumentation.org/packages/ggplot2/versions/1.0.0?utm_source=openai)).\\n\\nAnswer + (YYYY\u2011MM\u2011DD): **2014\u201105\u201121**.\"}\n\nevent: response.content_part.done\ndata: + {\"type\":\"response.content_part.done\",\"content_index\":0,\"item_id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"output_index\":1,\"part\":{\"type\":\"output_text\",\"annotations\":[{\"type\":\"url_citation\",\"end_index\":385,\"start_index\":295,\"title\":\"Index + of /CRAN/src/contrib/Archive/ggplot2\",\"url\":\"https://stat.ethz.ch/CRAN/src/contrib/Archive/ggplot2/?utm_source=openai\"},{\"type\":\"url_citation\",\"end_index\":570,\"start_index\":466,\"title\":\"ggplot2 + package - RDocumentation\",\"url\":\"https://www.rdocumentation.org/packages/ggplot2/versions/1.0.0?utm_source=openai\"}],\"logprobs\":[],\"text\":\"ggplot2 + version\u202F1.0.0 was released on CRAN on **2014\u201105\u201121**. This + is confirmed by both the CRAN archive listing and additional package metadata:\\n\\n- + The CRAN archive (Archive/ggplot2) shows the file `ggplot2_1.0.0.tar.gz` with + a timestamp of **2014\u201105\u201121 15:36**, indicating the date of release + ([stat.ethz.ch](https://stat.ethz.ch/CRAN/src/contrib/Archive/ggplot2/?utm_source=openai)).\\n- + RDocumentation also lists \u201CLast Published: May 21st, 2014\u201D for version\u202F1.0.0 + ([rdocumentation.org](https://www.rdocumentation.org/packages/ggplot2/versions/1.0.0?utm_source=openai)).\\n\\nAnswer + (YYYY\u2011MM\u2011DD): **2014\u201105\u201121**.\"},\"sequence_number\":23}\n\nevent: + response.output_item.done\ndata: {\"type\":\"response.output_item.done\",\"item\":{\"id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[{\"type\":\"url_citation\",\"end_index\":385,\"start_index\":295,\"title\":\"Index + of /CRAN/src/contrib/Archive/ggplot2\",\"url\":\"https://stat.ethz.ch/CRAN/src/contrib/Archive/ggplot2/?utm_source=openai\"},{\"type\":\"url_citation\",\"end_index\":570,\"start_index\":466,\"title\":\"ggplot2 + package - RDocumentation\",\"url\":\"https://www.rdocumentation.org/packages/ggplot2/versions/1.0.0?utm_source=openai\"}],\"logprobs\":[],\"text\":\"ggplot2 + version\u202F1.0.0 was released on CRAN on **2014\u201105\u201121**. This + is confirmed by both the CRAN archive listing and additional package metadata:\\n\\n- + The CRAN archive (Archive/ggplot2) shows the file `ggplot2_1.0.0.tar.gz` with + a timestamp of **2014\u201105\u201121 15:36**, indicating the date of release + ([stat.ethz.ch](https://stat.ethz.ch/CRAN/src/contrib/Archive/ggplot2/?utm_source=openai)).\\n- + RDocumentation also lists \u201CLast Published: May 21st, 2014\u201D for version\u202F1.0.0 + ([rdocumentation.org](https://www.rdocumentation.org/packages/ggplot2/versions/1.0.0?utm_source=openai)).\\n\\nAnswer + (YYYY\u2011MM\u2011DD): **2014\u201105\u201121**.\"}],\"role\":\"assistant\"},\"output_index\":1,\"sequence_number\":24}\n\nevent: + response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_0296f1f2e122877d0169fb7c5f6ab8819c90a597312ae510fa\",\"object\":\"response\",\"created_at\":1778089055,\"status\":\"completed\",\"background\":false,\"completed_at\":1778089058,\"error\":null,\"frequency_penalty\":0.0,\"incomplete_details\":null,\"instructions\":null,\"max_output_tokens\":null,\"max_tool_calls\":null,\"model\":\"gpt-4.1-2025-04-14\",\"moderation\":null,\"output\":[{\"id\":\"ws_0296f1f2e122877d0169fb7c5fc974819caa17a899327357e1\",\"type\":\"web_search_call\",\"status\":\"completed\",\"action\":{\"type\":\"search\",\"queries\":[\"ggplot2 + CRAN archive release dates\"],\"query\":\"ggplot2 CRAN archive release dates\"}},{\"id\":\"msg_0296f1f2e122877d0169fb7c617028819c86e025e1ea850e32\",\"type\":\"message\",\"status\":\"completed\",\"content\":[{\"type\":\"output_text\",\"annotations\":[{\"type\":\"url_citation\",\"end_index\":385,\"start_index\":295,\"title\":\"Index + of /CRAN/src/contrib/Archive/ggplot2\",\"url\":\"https://stat.ethz.ch/CRAN/src/contrib/Archive/ggplot2/?utm_source=openai\"},{\"type\":\"url_citation\",\"end_index\":570,\"start_index\":466,\"title\":\"ggplot2 + package - RDocumentation\",\"url\":\"https://www.rdocumentation.org/packages/ggplot2/versions/1.0.0?utm_source=openai\"}],\"logprobs\":[],\"text\":\"ggplot2 + version\u202F1.0.0 was released on CRAN on **2014\u201105\u201121**. This + is confirmed by both the CRAN archive listing and additional package metadata:\\n\\n- + The CRAN archive (Archive/ggplot2) shows the file `ggplot2_1.0.0.tar.gz` with + a timestamp of **2014\u201105\u201121 15:36**, indicating the date of release + ([stat.ethz.ch](https://stat.ethz.ch/CRAN/src/contrib/Archive/ggplot2/?utm_source=openai)).\\n- + RDocumentation also lists \u201CLast Published: May 21st, 2014\u201D for version\u202F1.0.0 + ([rdocumentation.org](https://www.rdocumentation.org/packages/ggplot2/versions/1.0.0?utm_source=openai)).\\n\\nAnswer + (YYYY\u2011MM\u2011DD): **2014\u201105\u201121**.\"}],\"role\":\"assistant\"}],\"parallel_tool_calls\":true,\"presence_penalty\":0.0,\"previous_response_id\":null,\"prompt_cache_key\":null,\"prompt_cache_retention\":\"in_memory\",\"reasoning\":{\"effort\":null,\"summary\":null},\"safety_identifier\":null,\"service_tier\":\"default\",\"store\":false,\"temperature\":1.0,\"text\":{\"format\":{\"type\":\"text\"},\"verbosity\":\"medium\"},\"tool_choice\":\"auto\",\"tools\":[{\"type\":\"web_search\",\"return_token_budget\":\"default\",\"search_context_size\":\"medium\",\"user_location\":{\"type\":\"approximate\",\"city\":null,\"country\":\"US\",\"region\":null,\"timezone\":null}}],\"top_logprobs\":0,\"top_p\":1.0,\"truncation\":\"disabled\",\"usage\":{\"input_tokens\":17248,\"input_tokens_details\":{\"cached_tokens\":0},\"output_tokens\":195,\"output_tokens_details\":{\"reasoning_tokens\":0},\"total_tokens\":17443},\"user\":null,\"metadata\":{}},\"sequence_number\":25}\n\n" + headers: + CF-Cache-Status: + - DYNAMIC + CF-Ray: + - 9f79c0f2fc80ad18-MSP + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Wed, 06 May 2026 17:37:35 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + openai-processing-ms: + - '174' + openai-version: + - '2020-10-01' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '30000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '29999648' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + status: + code: 200 + message: OK +- request: + body: '{"input": [{"role": "user", "content": [{"type": "input_text", "text": + "When was ggplot2 1.0.0 released to CRAN? Answer in YYYY-MM-DD format. The CRAN + archive page has this info."}]}, {"id": "ws_0296f1f2e122877d0169fb7c5fc974819caa17a899327357e1", + "action": {"query": "ggplot2 CRAN archive release dates", "type": "search", + "queries": ["ggplot2 CRAN archive release dates"], "sources": null}, "status": + "completed", "type": "web_search_call"}, {"role": "assistant", "content": [{"type": + "output_text", "text": "ggplot2 version\u202f1.0.0 was released on CRAN on **2014\u201105\u201121**. + This is confirmed by both the CRAN archive listing and additional package metadata:\n\n- + The CRAN archive (Archive/ggplot2) shows the file `ggplot2_1.0.0.tar.gz` with + a timestamp of **2014\u201105\u201121 15:36**, indicating the date of release + ([stat.ethz.ch](https://stat.ethz.ch/CRAN/src/contrib/Archive/ggplot2/?utm_source=openai)).\n- + RDocumentation also lists \u201cLast Published: May 21st, 2014\u201d for version\u202f1.0.0 + ([rdocumentation.org](https://www.rdocumentation.org/packages/ggplot2/versions/1.0.0?utm_source=openai)).\n\nAnswer + (YYYY\u2011MM\u2011DD): **2014\u201105\u201121**.", "annotations": []}], "status": + "completed", "type": "message", "id": "msg_missing_id"}, {"role": "user", "content": + [{"type": "input_text", "text": "What month was that?"}]}], "model": "gpt-4.1", + "store": false, "stream": true, "tools": [{"type": "web_search"}]}' + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '1361' + Content-Type: + - application/json + Cookie: + - __cf_bm=G7W_tql3Jz1vNh82LVKxvLurfbQX4Lkg2iRuJjgkhag-1778089055.1984863-1.0.1.1-wN5ndYvGHmykfj.4TYnHifuydRc3hnEV5Q8pCP7KyIftqalX.NfR5saduBVxd79JvCCDCdrDjRoVLFNVqFy5XVBkTcdXabOGRT33TWm9gXaCHEZcOrtgYbw.zcpT8gIE + Host: + - api.openai.com + X-Stainless-Async: + - 'false' + x-stainless-read-timeout: + - '600' + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: 'event: response.created + + data: {"type":"response.created","response":{"id":"resp_0296f1f2e122877d0169fb7c62a9a8819c830adc311c8ae6b1","object":"response","created_at":1778089058,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"web_search","return_token_budget":"default","search_context_size":"medium","user_location":{"type":"approximate","city":null,"country":"US","region":null,"timezone":null}}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":0} + + + event: response.in_progress + + data: {"type":"response.in_progress","response":{"id":"resp_0296f1f2e122877d0169fb7c62a9a8819c830adc311c8ae6b1","object":"response","created_at":1778089058,"status":"in_progress","background":false,"completed_at":null,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","moderation":null,"output":[],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"web_search","return_token_budget":"default","search_context_size":"medium","user_location":{"type":"approximate","city":null,"country":"US","region":null,"timezone":null}}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}},"sequence_number":1} + + + event: response.output_item.added + + data: {"type":"response.output_item.added","item":{"id":"msg_0296f1f2e122877d0169fb7c62fc80819caaf76a534dfcf396","type":"message","status":"in_progress","content":[],"role":"assistant"},"output_index":0,"sequence_number":2} + + + event: response.content_part.added + + data: {"type":"response.content_part.added","content_index":0,"item_id":"msg_0296f1f2e122877d0169fb7c62fc80819caaf76a534dfcf396","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""},"sequence_number":3} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"The","item_id":"msg_0296f1f2e122877d0169fb7c62fc80819caaf76a534dfcf396","logprobs":[],"obfuscation":"1vUdDPqmZxQkG","output_index":0,"sequence_number":4} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" month","item_id":"msg_0296f1f2e122877d0169fb7c62fc80819caaf76a534dfcf396","logprobs":[],"obfuscation":"yIIyxa2Rll","output_index":0,"sequence_number":5} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" was","item_id":"msg_0296f1f2e122877d0169fb7c62fc80819caaf76a534dfcf396","logprobs":[],"obfuscation":"Qi75tJh5GCpO","output_index":0,"sequence_number":6} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":" **","item_id":"msg_0296f1f2e122877d0169fb7c62fc80819caaf76a534dfcf396","logprobs":[],"obfuscation":"Akwl6WDKtfmEY","output_index":0,"sequence_number":7} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"May","item_id":"msg_0296f1f2e122877d0169fb7c62fc80819caaf76a534dfcf396","logprobs":[],"obfuscation":"0KM9kvfcZE0yV","output_index":0,"sequence_number":8} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":"**","item_id":"msg_0296f1f2e122877d0169fb7c62fc80819caaf76a534dfcf396","logprobs":[],"obfuscation":"EfArfwUPjFCuRv","output_index":0,"sequence_number":9} + + + event: response.output_text.delta + + data: {"type":"response.output_text.delta","content_index":0,"delta":".","item_id":"msg_0296f1f2e122877d0169fb7c62fc80819caaf76a534dfcf396","logprobs":[],"obfuscation":"h3bKCnaCJOF4jP5","output_index":0,"sequence_number":10} + + + event: response.output_text.done + + data: {"type":"response.output_text.done","content_index":0,"item_id":"msg_0296f1f2e122877d0169fb7c62fc80819caaf76a534dfcf396","logprobs":[],"output_index":0,"sequence_number":11,"text":"The + month was **May**."} + + + event: response.content_part.done + + data: {"type":"response.content_part.done","content_index":0,"item_id":"msg_0296f1f2e122877d0169fb7c62fc80819caaf76a534dfcf396","output_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"The + month was **May**."},"sequence_number":12} + + + event: response.output_item.done + + data: {"type":"response.output_item.done","item":{"id":"msg_0296f1f2e122877d0169fb7c62fc80819caaf76a534dfcf396","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The + month was **May**."}],"role":"assistant"},"output_index":0,"sequence_number":13} + + + event: response.completed + + data: {"type":"response.completed","response":{"id":"resp_0296f1f2e122877d0169fb7c62a9a8819c830adc311c8ae6b1","object":"response","created_at":1778089058,"status":"completed","background":false,"completed_at":1778089059,"error":null,"frequency_penalty":0.0,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4.1-2025-04-14","moderation":null,"output":[{"id":"msg_0296f1f2e122877d0169fb7c62fc80819caaf76a534dfcf396","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The + month was **May**."}],"role":"assistant"}],"parallel_tool_calls":true,"presence_penalty":0.0,"previous_response_id":null,"prompt_cache_key":null,"prompt_cache_retention":"in_memory","reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":false,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"web_search","return_token_budget":"default","search_context_size":"medium","user_location":{"type":"approximate","city":null,"country":"US","region":null,"timezone":null}}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":567,"input_tokens_details":{"cached_tokens":0},"output_tokens":9,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":576},"user":null,"metadata":{}},"sequence_number":14} + + + ' + headers: + CF-Cache-Status: + - DYNAMIC + CF-Ray: + - 9f79c1082900a215-MSP + Connection: + - keep-alive + Content-Type: + - text/event-stream; charset=utf-8 + Date: + - Wed, 06 May 2026 17:37:38 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + openai-processing-ms: + - '139' + openai-version: + - '2020-10-01' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '30000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '29999414' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 1ms + status: + code: 200 + message: OK +version: 1 diff --git a/tests/conftest.py b/tests/conftest.py index d6a01b0c..484795c2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -386,7 +386,7 @@ def assert_list_models(chat_fun: ChatFun): # --------------------------------------------------------------------------- -def assert_tool_web_fetch(chat_fun: ChatFun, tool, stream: bool = True): +def assert_tool_web_fetch(chat_fun: ChatFun, tool, stream: bool = True) -> Chat: """Test web fetch tool functionality.""" chat = chat_fun() chat.register_tool(tool) @@ -397,9 +397,10 @@ def assert_tool_web_fetch(chat_fun: ChatFun, tool, stream: bool = True): response = chat.chat("Who directed it?", stream=stream) assert "George Lucas" in str(response) + return chat -def assert_tool_web_search(chat_fun: ChatFun, tool, hint: str = "", stream: bool = True): +def assert_tool_web_search(chat_fun: ChatFun, tool, hint: str = "", stream: bool = True) -> Chat: """Test web search tool functionality.""" chat = chat_fun() chat.register_tool(tool) @@ -415,6 +416,7 @@ def assert_tool_web_search(chat_fun: ChatFun, tool, hint: str = "", stream: bool response = chat.chat("What month was that?", stream=stream) assert "May" in str(response) + return chat retry_api_call = retry( diff --git a/tests/test_otel.py b/tests/test_otel.py index 98ca9fc0..09bf8987 100644 --- a/tests/test_otel.py +++ b/tests/test_otel.py @@ -415,8 +415,7 @@ def gen(): return gen() chat.provider.chat_perform = streaming_perform # type: ignore[method-assign] - chat.provider.stream_content = lambda chunk: ContentText(text="hi") # type: ignore[method-assign] - chat.provider.stream_text = lambda chunk: "hi" # type: ignore[method-assign] + chat.provider.stream_content = lambda chunk: [ContentText(text="hi")] # type: ignore[method-assign] chat.provider.stream_merge_chunks = ( # type: ignore[method-assign] lambda result, chunk: chunk ) @@ -449,8 +448,7 @@ async def gen(): return gen() chat.provider.chat_perform_async = streaming_perform_async # type: ignore[method-assign] - chat.provider.stream_content = lambda chunk: ContentText(text="hi") # type: ignore[method-assign] - chat.provider.stream_text = lambda chunk: "hi" # type: ignore[method-assign] + chat.provider.stream_content = lambda chunk: [ContentText(text="hi")] # type: ignore[method-assign] chat.provider.stream_merge_chunks = ( # type: ignore[method-assign] lambda result, chunk: chunk ) diff --git a/tests/test_provider_anthropic.py b/tests/test_provider_anthropic.py index 55b34dd8..60fdf315 100644 --- a/tests/test_provider_anthropic.py +++ b/tests/test_provider_anthropic.py @@ -10,6 +10,7 @@ tool_web_fetch, tool_web_search, ) +from chatlas.types import ContentCitation, ContentText, ContentToolRequestSearch, ContentToolResponseFetch, ContentToolResponseSearch from chatlas._provider_anthropic import AnthropicProvider from pydantic import BaseModel, Field @@ -104,12 +105,52 @@ def chat_fun(**kwargs): **kwargs, ) - assert_tool_web_fetch(chat_fun, tool_web_fetch()) + chat = assert_tool_web_fetch(chat_fun, tool_web_fetch()) + fetched = [ + c + for turn in chat.get_turns() + for c in turn.contents + if isinstance(c, ContentToolResponseFetch) + ] + assert fetched and fetched[0].url + assert fetched[0].status == "success" @pytest.mark.vcr def test_anthropic_web_search(): - assert_tool_web_search(chat_func, tool_web_search()) + chat = assert_tool_web_search(chat_func, tool_web_search()) + results = [ + c + for turn in chat.get_turns() + for c in turn.contents + if isinstance(c, ContentToolResponseSearch) + ] + assert results and results[0].sources + assert all(s.url for s in results[0].sources) + assert any(s.title for s in results[0].sources) + + +@pytest.mark.vcr +def test_anthropic_web_search_streaming(): + chat = chat_func() + chat.register_tool(tool_web_search()) + items = list( + chat.stream( + "When was ggplot2 1.0.0 released to CRAN? Answer in YYYY-MM-DD format.", + content="all", + ) + ) + cites = [x for x in items if isinstance(x, ContentCitation)] + results = [x for x in items if isinstance(x, ContentToolResponseSearch)] + reqs = [x for x in items if isinstance(x, ContentToolRequestSearch)] + assert results and results[0].sources + assert reqs and reqs[0].query + assert cites and all(c.citation.url for c in cites) + answer = "".join(x for x in items if isinstance(x, str)) + assert all(c.citation.cited_text in answer for c in cites) + # interleaved: at least one citation is not the very last item + cite_idx = [i for i, x in enumerate(items) if isinstance(x, ContentCitation)] + assert cite_idx and min(cite_idx) < len(items) - 1 @pytest.mark.vcr @@ -132,6 +173,17 @@ def test_anthropic_web_search_citations(): has_citations = any(getattr(block, "citations", None) for block in text_blocks) assert has_citations, "Expected citations on text blocks from web search" + # Normalized: citations transferred onto ContentText + cites = [ + cit + for content in turn.contents + if isinstance(content, ContentText) + for cit in content.citations + ] + assert cites, "expected citations transferred onto ContentText" + assert all(c.url for c in cites) + assert any(c.cited_text for c in cites) + @pytest.mark.vcr def test_data_extraction(): diff --git a/tests/test_provider_google.py b/tests/test_provider_google.py index 0390b4a6..44ee300d 100644 --- a/tests/test_provider_google.py +++ b/tests/test_provider_google.py @@ -1,6 +1,13 @@ import pytest import requests from chatlas import ChatGoogle, ChatVertex, tool_web_fetch, tool_web_search +from chatlas.types import ( + ContentCitation, + ContentText, + ContentToolRequestSearch, + ContentToolResponseFetch, + ContentToolResponseSearch, +) from google.genai.errors import APIError from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential @@ -113,9 +120,9 @@ def test_name_setting(): # TODO: this test runs fine in isolation, but fails for some reason when run with the other tests # Seems google isn't handling async 100% correctly -#@pytest.mark.vcr -#@pytest.mark.asyncio -#async def test_google_simple_streaming_request(): +# @pytest.mark.vcr +# @pytest.mark.asyncio +# async def test_google_simple_streaming_request(): # chat = chat_func( # system_prompt="Be as terse as possible; no punctuation. Do not spell out numbers.", # ) @@ -178,13 +185,82 @@ def test_data_extraction(): @pytest.mark.vcr @retry_gemini_call def test_google_web_fetch(): - assert_tool_web_fetch(chat_func, tool_web_fetch()) + chat = assert_tool_web_fetch(chat_func, tool_web_fetch()) + fetched = [ + c + for turn in chat.get_turns() + for c in turn.contents + if isinstance(c, ContentToolResponseFetch) + ] + assert fetched and fetched[0].url + assert fetched[0].status == "success" + # The normalized status doesn't drop the provider-native value + assert fetched[0].extra is not None + assert "URL_RETRIEVAL_STATUS" in str(fetched[0].extra["url_metadata"]) @pytest.mark.vcr @retry_gemini_call def test_google_web_search(): - assert_tool_web_search(chat_func, tool_web_search()) + chat = assert_tool_web_search(chat_func, tool_web_search()) + search_requests = [ + c + for turn in chat.get_turns() + for c in turn.contents + if isinstance(c, ContentToolRequestSearch) + ] + results = [ + c + for turn in chat.get_turns() + for c in turn.contents + if isinstance(c, ContentToolResponseSearch) + ] + assert search_requests + assert results and results[0].sources + # Note: the cassette grounding chunks don't include a domain field, so + # domain is None for all sources in this recording. + cites = [ + cit + for turn in chat.get_turns() + for c in turn.contents + if isinstance(c, ContentText) + for cit in c.citations + ] + assert cites + # Google supplies the grounded span directly (segment.text) + assert all(c.cited_text for c in cites) + + +@pytest.mark.vcr +def test_google_web_search_streaming(): + chat = chat_func() + chat.register_tool(tool_web_search()) + items = list( + chat.stream( + "When was ggplot2 1.0.0 released to CRAN? Answer in YYYY-MM-DD format.", + content="all", + ) + ) + assert any(isinstance(x, ContentToolRequestSearch) for x in items) + results = [x for x in items if isinstance(x, ContentToolResponseSearch)] + assert results and results[0].sources + citations = [x for x in items if isinstance(x, ContentCitation)] + assert citations + assert all(c.citation.cited_text for c in citations) + + +@pytest.mark.vcr +def test_google_web_fetch_streaming(): + chat = chat_func() + chat.register_tool(tool_web_fetch()) + items = list( + chat.stream( + "What's the first movie listed on https://rvest.tidyverse.org/articles/starwars.html?", + content="all", + ) + ) + fetched = [x for x in items if isinstance(x, ContentToolResponseFetch)] + assert fetched and fetched[0].url and fetched[0].status == "success" @pytest.mark.vcr @@ -255,3 +331,27 @@ def test_google_thought_signature_roundtrip(): # Verify it round-trips back into the Part part = provider._as_part_type(req) assert part.thought_signature == fake_signature + + +def test_normalize_retrieval_status(): + from chatlas._provider_google import normalize_retrieval_status + + assert normalize_retrieval_status("URL_RETRIEVAL_STATUS_SUCCESS") == "success" + assert normalize_retrieval_status("URL_RETRIEVAL_STATUS_UNSPECIFIED") is None + # Every other reported status collapses to "error" (native value kept in extra) + assert normalize_retrieval_status("URL_RETRIEVAL_STATUS_ERROR") == "error" + assert normalize_retrieval_status("URL_RETRIEVAL_STATUS_PAYWALL") == "error" + assert normalize_retrieval_status("URL_RETRIEVAL_STATUS_UNSAFE") == "error" + assert normalize_retrieval_status(None) is None + + # Accepts the SDK enum (str-enum) as well as the raw string + from google.genai.types import UrlRetrievalStatus + + assert ( + normalize_retrieval_status(UrlRetrievalStatus.URL_RETRIEVAL_STATUS_SUCCESS) + == "success" + ) + assert ( + normalize_retrieval_status(UrlRetrievalStatus.URL_RETRIEVAL_STATUS_PAYWALL) + == "error" + ) diff --git a/tests/test_provider_openai.py b/tests/test_provider_openai.py index 75c9b9e3..e8b296a8 100644 --- a/tests/test_provider_openai.py +++ b/tests/test_provider_openai.py @@ -3,6 +3,7 @@ import httpx import pytest from chatlas import ChatOpenAI, tool_web_search +from chatlas.types import ContentCitation, ContentText, ContentToolRequestSearch from openai.types.responses import ResponseOutputMessage, ResponseOutputText from .conftest import ( @@ -82,12 +83,52 @@ def test_openai_web_search(): def chat_fun(**kwargs): return ChatOpenAI(model="gpt-4.1", **kwargs) - assert_tool_web_search( + chat = assert_tool_web_search( chat_fun, tool_web_search(), hint="The CRAN archive page has this info.", ) + # Web search citations should be surfaced on ContentText (OpenAI provides offsets) + cites = [ + cit + for turn in chat.get_turns() + for content in turn.contents + if isinstance(content, ContentText) + for cit in content.citations + ] + assert cites, "expected url_citation annotations surfaced as citations" + assert all(c.url for c in cites) + # cited_text is derived from the annotation offsets; every citation carries the span + answer = "".join( + content.text + for turn in chat.get_turns() + for content in turn.contents + if isinstance(content, ContentText) + ) + assert all(c.cited_text and c.cited_text in answer for c in cites) + + +@pytest.mark.vcr +def test_openai_web_search_streaming(): + chat = ChatOpenAI(model="gpt-4.1") + chat.register_tool(tool_web_search()) + items = list( + chat.stream( + "When was ggplot2 1.0.0 released to CRAN? Answer in YYYY-MM-DD format. The CRAN archive page has this info.", + content="all", + ) + ) + citations = [x for x in items if isinstance(x, ContentCitation)] + assert citations + assert all(c.citation.url for c in citations) + answer = "".join(x for x in items if isinstance(x, str)) + assert all(c.citation.cited_text and c.citation.cited_text in answer for c in citations) + # interleaved: at least one citation arrives before the last item in the stream + cite_idx = [i for i, x in enumerate(items) if isinstance(x, ContentCitation)] + assert cite_idx and min(cite_idx) < len(items) - 1 + assert any(isinstance(x, ContentToolRequestSearch) for x in items) + @pytest.mark.vcr def test_openai_images(): @@ -236,9 +277,7 @@ def make_response(action: dict): assert turn.contents[0].query == "find this" # search action without query but with queries - resp = make_response( - {"type": "search", "query": "", "queries": ["first query"]} - ) + resp = make_response({"type": "search", "query": "", "queries": ["first query"]}) turn = provider._response_as_turn(resp, has_data_model=False) assert isinstance(turn.contents[0], ContentToolRequestSearch) assert turn.contents[0].query == "first query" diff --git a/tests/test_provider_openai_completions.py b/tests/test_provider_openai_completions.py index 32f64e6e..018eb227 100644 --- a/tests/test_provider_openai_completions.py +++ b/tests/test_provider_openai_completions.py @@ -138,13 +138,11 @@ def __init__(self, choices): chunk = FakeChunk([FakeChoice(FakeDelta(reasoning_content="think"))]) result = provider.stream_content(chunk) - assert isinstance(result, ContentThinkingDelta) - assert result.thinking == "think" + assert result == [ContentThinkingDelta(thinking="think")] chunk = FakeChunk([FakeChoice(FakeDelta(content="hello"))]) result = provider.stream_content(chunk) - assert isinstance(result, ContentText) - assert result.text == "hello" + assert result == [ContentText(text="hello")] def test_response_as_turn_extracts_reasoning_content(): @@ -186,8 +184,7 @@ def __init__(self, choices): chunk = FakeChunk([FakeChoice(FakeDelta(reasoning="think"))]) result = provider.stream_content(chunk) - assert isinstance(result, ContentThinkingDelta) - assert result.thinking == "think" + assert result == [ContentThinkingDelta(thinking="think")] def test_response_as_turn_extracts_reasoning_field(): From a937c34972a606d954e374b8213c4f3218ff5b37 Mon Sep 17 00:00:00 2001 From: Carson Date: Fri, 12 Jun 2026 11:26:15 -0500 Subject: [PATCH 4/7] chore: update docs, CHANGELOG, and regenerate OpenAI type stubs - Document Citation, Source, and ContentCitation in the API reference - Add sidebar/quarto entries for the new citation types - Regenerate openai/_submit*.py type stubs to pick up the new moderation param - CHANGELOG entry for the citation content model feature --- CHANGELOG.md | 7 +++++++ docs/_quarto.yml | 3 +++ docs/_sidebar.yml | 9 ++++++--- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17cc7cc1..701fce92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [UNRELEASED] +### Breaking changes + +* `ContentToolResponseSearch.urls` (a `list[str]`) has been replaced by `.sources` (a `list[Source]`), where each `Source` carries the result's `url`, `title`, and `domain`. Code reading `.urls` should switch to `[s.url for s in x.sources]`. +* `Citation` no longer carries `start_index`/`end_index` (provider-native byte/character offsets). Use `cited_text` — a verbatim span of the answer — to locate a citation by string-matching instead. This is the normalized, cross-provider placement key. + ### New features * chatlas is now instrumented with [OpenTelemetry](https://opentelemetry.io/) (OTel) out of the box, making it much easier to see how your app behaves in production — where time goes, how many tokens you're spending, which tools run, and where things fail. Without writing any tracing code, you get spans that capture the full structure of a conversation as one connected trace: an `invoke_agent` span over the whole chat loop, a `chat` span per model call, and an `execute_tool` span per tool invocation, with attributes (token usage, response model/ID, tool errors) that follow the [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/). Because chatlas keeps its spans active during each call, HTTP spans from provider instrumentors and any spans your own tools emit nest underneath automatically. Point it at any OTel-compatible backend (Logfire, Datadog, Honeycomb, Jaeger, …); message content is omitted by default and opt-in via `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true`. See the [monitoring guide](https://posit-dev.github.io/chatlas/get-started/monitor.html) to get started. (#310) * `Chat` gains a `model` property to get (or set) the model after the chat is created. Setting it does not validate the model name. * `ChatGoogle()`'s `reasoning` parameter now accepts a string thinking level (`"minimal"`, `"low"`, `"medium"`, or `"high"`) in addition to an integer token budget. * `ChatAnthropic()`'s `reasoning` parameter now accepts a string effort level (`"low"`, `"medium"`, `"high"`, `"xhigh"`, or `"max"`) to enable Claude's adaptive thinking, in addition to an integer token budget. +* Web search and fetch answers now surface their citations. `ContentText` gained a `citations` list of the new `Citation` type, populated for OpenAI, Anthropic, and Google. Each `Citation` carries the link (`url`, optional `title`) plus `cited_text` — the span of the answer the source grounds — so a renderer can locate a citation by string-matching `cited_text` without provider-specific logic. `ContentToolResponseFetch` gained a normalized `status` field (`"success"`/`"error"`/`None`, with the provider-native reason preserved in `extra`), and web search results are now richer `Source` objects (see Breaking changes). The new `Citation` and `Source` types are exported from `chatlas.types`. +* When streaming with `content="all"`, web search/fetch results and citations are now emitted **progressively** — yielded during the stream rather than only available on the final turn. OpenAI and Anthropic interleave `ContentCitation` objects with the answer text as it streams; Google emits them when the response completes. The yielded objects include the web search/fetch content blocks plus `ContentCitation` instances (each wrapping a `Citation`). `ContentCitation` is exported from `chatlas.types`. ### Bug fixes diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 2ae7c892..4f5e2bc3 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -216,7 +216,9 @@ quartodoc: - Provider - title: User-facing types contents: + - types.Citation - types.Content + - types.ContentCitation - types.ContentImage - types.ContentImageInline - types.ContentImageRemote @@ -236,6 +238,7 @@ quartodoc: - types.ImageContentTypes - types.MISSING_TYPE - types.MISSING + - types.Source - types.SubmitInputArgsT - types.TokenUsage - types.ToolAnnotations diff --git a/docs/_sidebar.yml b/docs/_sidebar.yml index b836a730..18282197 100644 --- a/docs/_sidebar.yml +++ b/docs/_sidebar.yml @@ -26,9 +26,6 @@ website: - contents: - reference/Chat.qmd section: The chat object - - contents: - - reference/StreamController.qmd - section: Streaming - contents: - reference/content_image_file.qmd - reference/content_image_plot.qmd @@ -72,12 +69,17 @@ website: - reference/Provider.qmd section: Implement a model provider - contents: + - reference/types.Citation.qmd - reference/types.Content.qmd + - reference/types.ContentCitation.qmd - reference/types.ContentImage.qmd - reference/types.ContentImageInline.qmd - reference/types.ContentImageRemote.qmd - reference/types.ContentJson.qmd + - reference/types.ContentPDF.qmd - reference/types.ContentText.qmd + - reference/types.ContentThinking.qmd + - reference/types.ContentThinkingDelta.qmd - reference/types.ContentToolRequest.qmd - reference/types.ContentToolResult.qmd - reference/types.ContentToolRequestSearch.qmd @@ -89,6 +91,7 @@ website: - reference/types.ImageContentTypes.qmd - reference/types.MISSING_TYPE.qmd - reference/types.MISSING.qmd + - reference/types.Source.qmd - reference/types.SubmitInputArgsT.qmd - reference/types.TokenUsage.qmd - reference/types.ToolAnnotations.qmd From 971f56b3892f0729d6ae612002c886bcc8431333 Mon Sep 17 00:00:00 2001 From: Carson Date: Fri, 12 Jun 2026 11:32:03 -0500 Subject: [PATCH 5/7] =?UTF-8?q?docs(changelog):=20reorganize=20UNRELEASED?= =?UTF-8?q?=20=E2=80=94=20citations=20under=20New=20Features,=20Breaking?= =?UTF-8?q?=20Changes=20last?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 701fce92..ca89179f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,24 +9,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [UNRELEASED] -### Breaking changes - -* `ContentToolResponseSearch.urls` (a `list[str]`) has been replaced by `.sources` (a `list[Source]`), where each `Source` carries the result's `url`, `title`, and `domain`. Code reading `.urls` should switch to `[s.url for s in x.sources]`. -* `Citation` no longer carries `start_index`/`end_index` (provider-native byte/character offsets). Use `cited_text` — a verbatim span of the answer — to locate a citation by string-matching instead. This is the normalized, cross-provider placement key. - ### New features * chatlas is now instrumented with [OpenTelemetry](https://opentelemetry.io/) (OTel) out of the box, making it much easier to see how your app behaves in production — where time goes, how many tokens you're spending, which tools run, and where things fail. Without writing any tracing code, you get spans that capture the full structure of a conversation as one connected trace: an `invoke_agent` span over the whole chat loop, a `chat` span per model call, and an `execute_tool` span per tool invocation, with attributes (token usage, response model/ID, tool errors) that follow the [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/). Because chatlas keeps its spans active during each call, HTTP spans from provider instrumentors and any spans your own tools emit nest underneath automatically. Point it at any OTel-compatible backend (Logfire, Datadog, Honeycomb, Jaeger, …); message content is omitted by default and opt-in via `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true`. See the [monitoring guide](https://posit-dev.github.io/chatlas/get-started/monitor.html) to get started. (#310) +* Web search and fetch results now surface their citations across all three providers (OpenAI, Anthropic, Google), both progressively during streaming and on the final turn: + * When streaming with `content="all"`, citations are emitted as they arrive — OpenAI and Anthropic interleave `ContentCitation` objects with the answer text as it streams; Google emits them when the response completes. `ContentCitation` is exported from `chatlas.types`. + * `ContentText` gained a `citations` list of `Citation` objects. Each `Citation` carries `url`, optional `title`, and `cited_text` — a verbatim answer span that lets renderers locate sources by string-matching without provider-specific logic. `ContentToolResponseFetch` gained a normalized `status` field, and web search results are now richer `Source` objects (see Breaking changes). `Citation` and `Source` are exported from `chatlas.types`. * `Chat` gains a `model` property to get (or set) the model after the chat is created. Setting it does not validate the model name. * `ChatGoogle()`'s `reasoning` parameter now accepts a string thinking level (`"minimal"`, `"low"`, `"medium"`, or `"high"`) in addition to an integer token budget. * `ChatAnthropic()`'s `reasoning` parameter now accepts a string effort level (`"low"`, `"medium"`, `"high"`, `"xhigh"`, or `"max"`) to enable Claude's adaptive thinking, in addition to an integer token budget. -* Web search and fetch answers now surface their citations. `ContentText` gained a `citations` list of the new `Citation` type, populated for OpenAI, Anthropic, and Google. Each `Citation` carries the link (`url`, optional `title`) plus `cited_text` — the span of the answer the source grounds — so a renderer can locate a citation by string-matching `cited_text` without provider-specific logic. `ContentToolResponseFetch` gained a normalized `status` field (`"success"`/`"error"`/`None`, with the provider-native reason preserved in `extra`), and web search results are now richer `Source` objects (see Breaking changes). The new `Citation` and `Source` types are exported from `chatlas.types`. -* When streaming with `content="all"`, web search/fetch results and citations are now emitted **progressively** — yielded during the stream rather than only available on the final turn. OpenAI and Anthropic interleave `ContentCitation` objects with the answer text as it streams; Google emits them when the response completes. The yielded objects include the web search/fetch content blocks plus `ContentCitation` instances (each wrapping a `Citation`). `ContentCitation` is exported from `chatlas.types`. ### Bug fixes * OpenAI-compatible providers (e.g., `ChatOllama()` with models like qwen3) now capture thinking content returned in a `reasoning` field, not just `reasoning_content`. Previously this thinking content was silently dropped. +### Breaking changes + +* `ContentToolResponseSearch.urls` (a `list[str]`) has been replaced by `.sources` (a `list[Source]`), where each `Source` carries the result's `url`, `title`, and `domain`. Code reading `.urls` should switch to `[s.url for s in x.sources]`. +* `Citation` no longer carries `start_index`/`end_index` (provider-native byte/character offsets). Use `cited_text` — a verbatim span of the answer — to locate a citation by string-matching instead. This is the normalized, cross-provider placement key. + ## [0.18.1] - 2026-05-21 ### Improvements From 27f9520e10a2df2188cc264fc69010850cdc441c Mon Sep 17 00:00:00 2001 From: Carson Date: Fri, 12 Jun 2026 12:54:55 -0500 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20simplify=20citation=20content?= =?UTF-8?q?=20model=20=E2=80=94=20single=20ContentCitation=20type=20with?= =?UTF-8?q?=20flat=20url/title?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the dual citation representation (Citation class + ContentText.citations vs ContentCitation) and unify on ContentCitation as the sole citation type in both streaming and final turns. ContentCitation now carries url and title directly (no wrapper); stream position is the placement signal. --- CHANGELOG.md | 6 +-- chatlas/_content.py | 49 +++---------------- chatlas/_provider_anthropic.py | 35 ++++---------- chatlas/_provider_google.py | 64 ++++-------------------- chatlas/_provider_openai.py | 34 ++++--------- chatlas/types/__init__.py | 2 - docs/_quarto.yml | 1 - docs/_sidebar.yml | 1 - tests/test_content.py | 83 ++++++++++++++------------------ tests/test_provider_anthropic.py | 31 +++--------- tests/test_provider_google.py | 13 ++--- tests/test_provider_openai.py | 25 +++------- tests/test_turn_accumulator.py | 9 ++-- 13 files changed, 98 insertions(+), 255 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca89179f..49059b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * chatlas is now instrumented with [OpenTelemetry](https://opentelemetry.io/) (OTel) out of the box, making it much easier to see how your app behaves in production — where time goes, how many tokens you're spending, which tools run, and where things fail. Without writing any tracing code, you get spans that capture the full structure of a conversation as one connected trace: an `invoke_agent` span over the whole chat loop, a `chat` span per model call, and an `execute_tool` span per tool invocation, with attributes (token usage, response model/ID, tool errors) that follow the [OTel GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/). Because chatlas keeps its spans active during each call, HTTP spans from provider instrumentors and any spans your own tools emit nest underneath automatically. Point it at any OTel-compatible backend (Logfire, Datadog, Honeycomb, Jaeger, …); message content is omitted by default and opt-in via `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true`. See the [monitoring guide](https://posit-dev.github.io/chatlas/get-started/monitor.html) to get started. (#310) * Web search and fetch results now surface their citations across all three providers (OpenAI, Anthropic, Google), both progressively during streaming and on the final turn: - * When streaming with `content="all"`, citations are emitted as they arrive — OpenAI and Anthropic interleave `ContentCitation` objects with the answer text as it streams; Google emits them when the response completes. `ContentCitation` is exported from `chatlas.types`. - * `ContentText` gained a `citations` list of `Citation` objects. Each `Citation` carries `url`, optional `title`, and `cited_text` — a verbatim answer span that lets renderers locate sources by string-matching without provider-specific logic. `ContentToolResponseFetch` gained a normalized `status` field, and web search results are now richer `Source` objects (see Breaking changes). `Citation` and `Source` are exported from `chatlas.types`. + * When streaming with `content="all"`, `ContentCitation` objects are emitted as citations arrive — interleaved with text for OpenAI and Anthropic, at stream-end for Google. `ContentCitation` carries `url` and optional `title`; its position in the stream (relative to surrounding text) is the placement signal for rendering footnote markers. + * On the final turn, `ContentCitation` items appear in the turn's `contents` list after the `ContentText` they ground, in the same order as during streaming. `ContentCitation` and `Source` are exported from `chatlas.types`. + * `ContentToolResponseFetch` gained a normalized `status` field, and web search results are now richer `Source` objects (see Breaking changes). * `Chat` gains a `model` property to get (or set) the model after the chat is created. Setting it does not validate the model name. * `ChatGoogle()`'s `reasoning` parameter now accepts a string thinking level (`"minimal"`, `"low"`, `"medium"`, or `"high"`) in addition to an integer token budget. * `ChatAnthropic()`'s `reasoning` parameter now accepts a string effort level (`"low"`, `"medium"`, `"high"`, `"xhigh"`, or `"max"`) to enable Claude's adaptive thinking, in addition to an integer token budget. @@ -26,7 +27,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking changes * `ContentToolResponseSearch.urls` (a `list[str]`) has been replaced by `.sources` (a `list[Source]`), where each `Source` carries the result's `url`, `title`, and `domain`. Code reading `.urls` should switch to `[s.url for s in x.sources]`. -* `Citation` no longer carries `start_index`/`end_index` (provider-native byte/character offsets). Use `cited_text` — a verbatim span of the answer — to locate a citation by string-matching instead. This is the normalized, cross-provider placement key. ## [0.18.1] - 2026-05-21 diff --git a/chatlas/_content.py b/chatlas/_content.py index 119103e1..c3f0dfc3 100644 --- a/chatlas/_content.py +++ b/chatlas/_content.py @@ -171,35 +171,6 @@ def _repr_markdown_(self): return self.__str__() -class Citation(BaseModel): - """ - A source that grounds part of an assistant's answer. - - Produced by built-in web search/fetch tools. - - To render a citation, use ``url``/``title`` (the link) together with - ``cited_text`` (the placement key). ``cited_text`` is a verbatim span of - the assistant's answer that this source grounds; string-match it within the - answer text to position a marker, regardless of provider or streaming order. - It also doubles as the highlightable span shown to the user. - - Parameters - ---------- - url - Link to the cited source. - title - Title of the cited source, when the provider supplies one. - cited_text - A verbatim span of the assistant's answer that this source grounds. - The placement key: a consumer matches it to position a citation marker, - and it doubles as the highlightable span. - """ - - url: str - title: Optional[str] = None - cited_text: Optional[str] = None - - class Source(BaseModel): """A page surfaced by a web search (not necessarily cited in the answer).""" @@ -214,7 +185,6 @@ class ContentText(Content): """ text: str - citations: list[Citation] = [] content_type: ContentTypeEnum = "text" def __init__(self, **data: Any): @@ -226,10 +196,7 @@ def __init__(self, **data: Any): def __add__(self, other: object) -> "ContentText": if not isinstance(other, ContentText): return NotImplemented # type: ignore[return-value] - return ContentText.model_construct( - text=self.text + other.text, - citations=[*self.citations, *other.citations], - ) + return ContentText.model_construct(text=self.text + other.text) def __str__(self): return self.text @@ -888,19 +855,19 @@ def __str__(self): class ContentCitation(Content): """ - A citation emitted during streaming (``content="all"``). + A citation emitted during streaming and stored on the final turn. - Mirrors the final-turn citation data (which lives on - ``ContentText.citations``). Emitted when a citation becomes known: mid-stream - for Anthropic (applies to the text streamed so far), at turn-end for - OpenAI/Google (``cited_text`` derived from annotation offsets). + Position in the turn's contents list (relative to surrounding + ``ContentText`` items) is the placement signal: a consumer renders + a citation marker at the text accumulated so far. """ - citation: Citation + url: str + title: Optional[str] = None content_type: ContentTypeEnum = "citation" def __str__(self) -> str: - return f"[citation]: {self.citation.url}" + return f"[citation]: {self.url}" ContentUnion = Union[ diff --git a/chatlas/_provider_anthropic.py b/chatlas/_provider_anthropic.py index 0dcd3896..2da26011 100644 --- a/chatlas/_provider_anthropic.py +++ b/chatlas/_provider_anthropic.py @@ -19,7 +19,6 @@ from ._chat import Chat from ._content import ( - Citation, Content, ContentCitation, ContentImageInline, @@ -528,9 +527,6 @@ def _structured_tool_call(**kwargs: Any): def stream_content(self, chunk) -> list[Content]: if chunk.type == "content_block_delta": if chunk.delta.type == "text_delta": - text_block = getattr(self, "_streaming_text_block", None) - if text_block is not None and chunk.index == text_block["index"]: - text_block["text"] += chunk.delta.text return [ContentText.model_construct(text=chunk.delta.text)] if chunk.delta.type == "thinking_delta": return [ContentThinkingDelta(thinking=chunk.delta.thinking)] @@ -549,7 +545,6 @@ def stream_content(self, chunk) -> list[Content]: if btype == "text": self._streaming_text_block: Optional[dict] = { "index": chunk.index, - "text": "", "citations": [], } if ( @@ -592,7 +587,6 @@ def stream_content(self, chunk) -> list[Content]: result: list[Content] = [] text_block = getattr(self, "_streaming_text_block", None) if text_block is not None and chunk.index == text_block["index"]: - full_text = text_block["text"] buffered_citations = text_block["citations"] self._streaming_text_block = None for c in buffered_citations: @@ -600,11 +594,8 @@ def stream_content(self, chunk) -> list[Content]: if url: result.append( ContentCitation( - citation=Citation( - url=url, - title=getattr(c, "title", None), - cited_text=full_text, - ) + url=url, + title=getattr(c, "title", None), ) ) tracked = getattr(self, "_streaming_server_tool_use", None) @@ -780,7 +771,11 @@ def _as_message_params(self, turns: Sequence[Turn]) -> list["MessageParam"]: if not isinstance(turn, (UserTurn, AssistantTurn)): raise ValueError(f"Unknown role {turn.role}") - content = [self._as_content_block(c) for c in turn.contents] + content = [ + self._as_content_block(c) + for c in turn.contents + if not isinstance(c, ContentCitation) + ] # Drop empty assistant turns to avoid an API error # (all messages must have non-empty content) @@ -919,27 +914,17 @@ def _as_turn(self, completion: Message, has_data_model=False) -> AssistantTurn: if uses_new_output_format: contents.append(ContentJson(value=orjson.loads(content.text))) else: - citations = [] + contents.append(ContentText(text=content.text)) for c in content.citations or []: - # Anthropic citation locations are a union; only - # web_search_result_location carries a `url`. Document- - # grounding location types (char/page/content-block) and - # custom-search-result locations have no url and are skipped. url = getattr(c, "url", None) if not url: continue - # `cited_text` is normalized to the *answer* span (this - # text block), matching OpenAI/Google, so it can be located - # in the reply. (Anthropic's own cited_text is the source - # quote — that lives on the raw block if needed.) - citations.append( - Citation( + contents.append( + ContentCitation( url=url, title=getattr(c, "title", None), - cited_text=content.text, ) ) - contents.append(ContentText(text=content.text, citations=citations)) elif content.type == "tool_use": if uses_old_tool_approach and content.name == "_structured_tool_call": if not isinstance(content.input, dict): diff --git a/chatlas/_provider_google.py b/chatlas/_provider_google.py index 49907791..879ef554 100644 --- a/chatlas/_provider_google.py +++ b/chatlas/_provider_google.py @@ -8,7 +8,6 @@ from ._chat import Chat from ._content import ( - Citation, Content, ContentCitation, ContentImageInline, @@ -544,6 +543,7 @@ def _google_contents(self, turns: list[Turn]) -> list["GoogleContent"]: ContentToolResponseSearch, ContentToolRequestFetch, ContentToolResponseFetch, + ContentCitation, ), ) ] @@ -638,7 +638,6 @@ def _as_turn( ) contents: list[Content] = [] - text_contents: list[ContentText] = [] for part in parts: text = part.get("text") if text: @@ -647,9 +646,7 @@ def _as_turn( elif part.get("thought"): contents.append(ContentThinking(thinking=text)) else: - tc = ContentText(text=text) - text_contents.append(tc) - contents.append(tc) + contents.append(ContentText(text=text)) function_call = part.get("function_call") if function_call: # Seems name is required but id is optional? @@ -698,7 +695,7 @@ def _as_turn( if isinstance(finish_reason, FinishReason): finish_reason = finish_reason.name - search_contents = google_grounding_contents(grounding_metadata, text_contents) + search_contents = google_grounding_contents(grounding_metadata) url_ctx_contents = google_url_context_contents(url_context_metadata) contents = search_contents + contents + url_ctx_contents @@ -757,9 +754,8 @@ def supported_model_params(self) -> set[StandardModelParamNames]: def google_grounding_contents( grounding_metadata: "GroundingMetadataDict | None", - text_contents: list[ContentText], ) -> list[Content]: - """Build request/results content and attach citations from Google grounding metadata.""" + """Build search request/results + citations from Google grounding metadata.""" if not grounding_metadata: return [] @@ -775,7 +771,6 @@ def google_grounding_contents( ) chunks = grounding_metadata.get("grounding_chunks") or [] - # Index-aligned with `chunks` so grounding_chunk_indices resolve correctly. chunk_sources: list[Source] = [] for ch in chunks: web = ch.get("web") or {} @@ -786,8 +781,6 @@ def google_grounding_contents( domain=web.get("domain"), ) ) - # Only expose sources that have a usable URL (consistent with other providers, - # which never emit an empty-URL Source). display_sources = [s for s in chunk_sources if s.url] if display_sources: out.append( @@ -797,35 +790,11 @@ def google_grounding_contents( ) ) - # NOTE: Google segment offsets are UTF-8 byte offsets. They equal character - # offsets for ASCII but will be off for multi-byte text; treat as approximate - # for non-ASCII. Proper conversion would need text.encode("utf-8")[s:e].decode(). - supports = grounding_metadata.get("grounding_supports") or [] - for sup in supports: - seg = sup.get("segment") or {} - part_index = seg.get("part_index") - if part_index is None: - part_index = 0 - if part_index >= len(text_contents): - continue - target = text_contents[part_index] - # Google provides the grounded span directly as `segment.text`; prefer it - # as the (encoding-proof) cited_text over the byte offsets below. - cited_text = seg.get("text") + for sup in grounding_metadata.get("grounding_supports") or []: for idx in dict.fromkeys(sup.get("grounding_chunk_indices") or []): - if idx >= len(chunk_sources): - continue - src = chunk_sources[idx] - if not src.url: - continue - # NOTE: mutates the shared ContentText already present in `contents`. - target.citations.append( - Citation( - url=src.url, - title=src.title, - cited_text=cited_text, - ) - ) + if 0 <= idx < len(chunk_sources) and chunk_sources[idx].url: + src = chunk_sources[idx] + out.append(ContentCitation(url=src.url, title=src.title)) return out @@ -833,12 +802,7 @@ def google_grounding_contents( def google_grounding_stream_contents( grounding_metadata: "GroundingMetadataDict", ) -> list[Content]: - """Build search request/results + standalone ContentCitations from grounding (streaming). - - Called when a streamed chunk carries grounding_supports. Unlike the non-streaming - path (google_grounding_contents), there are no ContentText objects to attach - citations to, so citations are emitted as standalone ContentCitation items. - """ + """Build search request/results + ContentCitations from grounding (streaming).""" out: list[Content] = [] queries = grounding_metadata.get("web_search_queries") or [] @@ -871,18 +835,10 @@ def google_grounding_stream_contents( ) for sup in grounding_metadata.get("grounding_supports") or []: - seg = sup.get("segment") or {} - cited_text = seg.get("text") for idx in dict.fromkeys(sup.get("grounding_chunk_indices") or []): if 0 <= idx < len(chunk_sources) and chunk_sources[idx].url: src = chunk_sources[idx] - out.append( - ContentCitation( - citation=Citation( - url=src.url, title=src.title, cited_text=cited_text - ) - ) - ) + out.append(ContentCitation(url=src.url, title=src.title)) return out diff --git a/chatlas/_provider_openai.py b/chatlas/_provider_openai.py index 070b2436..cddba7ed 100644 --- a/chatlas/_provider_openai.py +++ b/chatlas/_provider_openai.py @@ -15,7 +15,6 @@ from ._chat import Chat from ._content import ( - Citation, Content, ContentCitation, ContentImageInline, @@ -308,28 +307,16 @@ def _chat_perform_args( def stream_content(self, chunk) -> list[Content]: if chunk.type == "response.output_text.delta": # https://platform.openai.com/docs/api-reference/responses-streaming/response/output_text/delta - self._streaming_text = getattr(self, "_streaming_text", "") + ( - chunk.delta or "" - ) return [ContentText.model_construct(text=chunk.delta)] if chunk.type == "response.output_text.annotation.added": # https://platform.openai.com/docs/api-reference/responses-streaming/response/output_text/annotation_added # annotation is a plain dict at runtime (SDK types it as `object`) ann: dict = chunk.annotation # type: ignore[assignment] if ann.get("type") == "url_citation": - text = getattr(self, "_streaming_text", "") - start = ann.get("start_index") - end = ann.get("end_index") - cited = ( - text[start:end] if (start is not None and end is not None) else None - ) return [ ContentCitation( - citation=Citation( - url=ann["url"], - title=ann.get("title"), - cited_text=cited, - ) + url=ann["url"], + title=ann.get("title"), ) ] return [] @@ -354,7 +341,6 @@ def stream_content(self, chunk) -> list[Content]: def stream_merge_chunks(self, completion, chunk): if chunk.type == "response.completed": - self._streaming_text = "" return chunk.response elif chunk.type == "response.failed": error = chunk.response.error @@ -429,21 +415,16 @@ def _response_as_turn(completion: Response, has_data_model: bool) -> AssistantTu data = orjson.loads(x.text) contents.append(ContentJson(value=data)) else: - citations = [] + contents.append(ContentText(text=x.text)) for a in x.annotations or []: if not isinstance(a, AnnotationURLCitation): continue - # OpenAI gives offsets but no cited_text; derive the - # grounded span by slicing the block text so every - # provider's citation carries cited_text uniformly. - citations.append( - Citation( + contents.append( + ContentCitation( url=a.url, title=a.title, - cited_text=x.text[a.start_index : a.end_index], ) ) - contents.append(ContentText(text=x.text, citations=citations)) elif output.type == "function_call": args = load_tool_request_args(output.arguments, output.name) @@ -511,7 +492,10 @@ def _response_as_turn(completion: Response, has_data_model: bool) -> AssistantTu def _turns_as_inputs(self, turns: list[Turn]) -> "list[ResponseInputItemParam]": res: "list[ResponseInputItemParam]" = [] for turn in turns: - res.extend([as_input_param(x, turn.role) for x in turn.contents]) + for x in turn.contents: + if isinstance(x, ContentCitation): + continue + res.append(as_input_param(x, turn.role)) return res def translate_model_params(self, params: StandardModelParams) -> "SubmitInputArgs": diff --git a/chatlas/types/__init__.py b/chatlas/types/__init__.py index 73ab3a5b..ce63054f 100644 --- a/chatlas/types/__init__.py +++ b/chatlas/types/__init__.py @@ -4,7 +4,6 @@ SubmitInputArgsT, ) from .._content import ( - Citation, Content, ContentCitation, ContentImage, @@ -32,7 +31,6 @@ from .._utils import MISSING, MISSING_TYPE __all__ = ( - "Citation", "Content", "ContentCitation", "ContentImage", diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 4f5e2bc3..7f6f2369 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -216,7 +216,6 @@ quartodoc: - Provider - title: User-facing types contents: - - types.Citation - types.Content - types.ContentCitation - types.ContentImage diff --git a/docs/_sidebar.yml b/docs/_sidebar.yml index 18282197..210d52e4 100644 --- a/docs/_sidebar.yml +++ b/docs/_sidebar.yml @@ -69,7 +69,6 @@ website: - reference/Provider.qmd section: Implement a model provider - contents: - - reference/types.Citation.qmd - reference/types.Content.qmd - reference/types.ContentCitation.qmd - reference/types.ContentImage.qmd diff --git a/tests/test_content.py b/tests/test_content.py index 4de69083..9b65d487 100644 --- a/tests/test_content.py +++ b/tests/test_content.py @@ -1,7 +1,6 @@ import pytest from chatlas import ChatOpenAI from chatlas._content import ( - Citation, ContentText, ContentToolResponseFetch, ContentToolResponseSearch, @@ -20,20 +19,6 @@ def test_invalid_inputs_give_useful_errors(): chat.chat(True) # type: ignore -def test_citation_defaults(): - c = Citation(url="https://python.org") - assert c.url == "https://python.org" - assert c.title is None - assert c.cited_text is None - - -def test_citation_has_no_offsets(): - c = Citation(url="https://a.com", title="A", cited_text="span") - assert c.cited_text == "span" - assert not hasattr(c, "start_index") - assert not hasattr(c, "end_index") - - def test_source_defaults(): s = Source(url="https://python.org") assert s.url == "https://python.org" @@ -41,20 +26,6 @@ def test_source_defaults(): assert s.domain is None -def test_contenttext_citations_default_empty(): - t = ContentText(text="hello") - assert t.citations == [] - - -def test_contenttext_with_citations(): - t = ContentText( - text="Python 3.14 is the latest release.", - citations=[Citation(url="https://docs.python.org", title="docs", cited_text="Python 3.14")], - ) - assert len(t.citations) == 1 - assert t.citations[0].cited_text == "Python 3.14" - - def test_search_results_use_sources(): r = ContentToolResponseSearch( sources=[Source(url="https://python.org", title="Python", domain="python.org")] @@ -77,36 +48,54 @@ def test_search_results_roundtrip(): assert restored.sources[0].url == "https://a.com" -def test_citation_source_exported_from_types(): - from chatlas.types import Citation, Source # noqa: F401 +def test_source_exported_from_types(): + from chatlas.types import Source # noqa: F401 import chatlas - # Not re-exported at top level (per design decision) assert not hasattr(chatlas, "Citation") -def test_contenttext_add_merges_citations(): - a = ContentText(text="foo", citations=[Citation(url="https://a.com")]) - b = ContentText(text="bar", citations=[Citation(url="https://b.com")]) - merged = a + b - assert merged.text == "foobar" - assert [c.url for c in merged.citations] == ["https://a.com", "https://b.com"] +def test_content_citation_fields(): + from chatlas._content import ContentCitation + c = ContentCitation(url="https://python.org", title="Python") + assert c.url == "https://python.org" + assert c.title == "Python" + assert c.content_type == "citation" + assert "https://python.org" in str(c) -def test_content_citation_wraps_citation(): - from chatlas._content import Citation, ContentCitation - c = ContentCitation(citation=Citation(url="https://a.com", title="A", cited_text="snippet")) - assert c.citation.url == "https://a.com" - assert c.content_type == "citation" - assert "https://a.com" in str(c) +def test_content_citation_title_optional(): + from chatlas._content import ContentCitation + c = ContentCitation(url="https://a.com") + assert c.title is None def test_content_citation_roundtrip(): - from chatlas._content import Citation, ContentCitation, create_content - c = ContentCitation(citation=Citation(url="https://a.com", cited_text="foo")) + from chatlas._content import ContentCitation, create_content + c = ContentCitation(url="https://a.com", title="A") restored = create_content(c.model_dump()) assert isinstance(restored, ContentCitation) - assert restored.citation.cited_text == "foo" + assert restored.url == "https://a.com" + assert restored.title == "A" def test_content_citation_exported_from_types(): from chatlas.types import ContentCitation # noqa: F401 + + +def test_citation_class_not_exported(): + import chatlas.types + assert not hasattr(chatlas.types, "Citation") + + +def test_contenttext_has_no_citations_field(): + from chatlas._content import ContentText + t = ContentText(text="hello") + assert not hasattr(t, "citations") + + +def test_contenttext_add_no_citations(): + from chatlas._content import ContentText + a = ContentText(text="foo") + b = ContentText(text="bar") + merged = a + b + assert merged.text == "foobar" diff --git a/tests/test_provider_anthropic.py b/tests/test_provider_anthropic.py index 60fdf315..bca673b0 100644 --- a/tests/test_provider_anthropic.py +++ b/tests/test_provider_anthropic.py @@ -10,7 +10,7 @@ tool_web_fetch, tool_web_search, ) -from chatlas.types import ContentCitation, ContentText, ContentToolRequestSearch, ContentToolResponseFetch, ContentToolResponseSearch +from chatlas.types import ContentCitation, ContentToolRequestSearch, ContentToolResponseFetch, ContentToolResponseSearch from chatlas._provider_anthropic import AnthropicProvider from pydantic import BaseModel, Field @@ -145,9 +145,7 @@ def test_anthropic_web_search_streaming(): reqs = [x for x in items if isinstance(x, ContentToolRequestSearch)] assert results and results[0].sources assert reqs and reqs[0].query - assert cites and all(c.citation.url for c in cites) - answer = "".join(x for x in items if isinstance(x, str)) - assert all(c.citation.cited_text in answer for c in cites) + assert cites and all(c.url for c in cites) # interleaved: at least one citation is not the very last item cite_idx = [i for i, x in enumerate(items) if isinstance(x, ContentCitation)] assert cite_idx and min(cite_idx) < len(items) - 1 @@ -155,34 +153,17 @@ def test_anthropic_web_search_streaming(): @pytest.mark.vcr def test_anthropic_web_search_citations(): - """Test that citations from web search are preserved on the completion.""" + """Test that citations from web search are ContentCitation items in the turn.""" chat = chat_func() chat.register_tool(tool_web_search()) chat.chat("When was ggplot2 1.0.0 released to CRAN? Answer in YYYY-MM-DD format.") - # Get the turn and verify citations are on the completion turn = chat.get_last_turn() assert turn is not None - assert turn.completion is not None - - # Find a text content block that should have citations - text_blocks = [c for c in turn.completion.content if c.type == "text"] - assert len(text_blocks) > 0 - - # At least one text block should have citations from web search - has_citations = any(getattr(block, "citations", None) for block in text_blocks) - assert has_citations, "Expected citations on text blocks from web search" - - # Normalized: citations transferred onto ContentText - cites = [ - cit - for content in turn.contents - if isinstance(content, ContentText) - for cit in content.citations - ] - assert cites, "expected citations transferred onto ContentText" + + cites = [c for c in turn.contents if isinstance(c, ContentCitation)] + assert cites, "expected ContentCitation items in turn contents" assert all(c.url for c in cites) - assert any(c.cited_text for c in cites) @pytest.mark.vcr diff --git a/tests/test_provider_google.py b/tests/test_provider_google.py index 44ee300d..4fdc6caa 100644 --- a/tests/test_provider_google.py +++ b/tests/test_provider_google.py @@ -3,7 +3,6 @@ from chatlas import ChatGoogle, ChatVertex, tool_web_fetch, tool_web_search from chatlas.types import ( ContentCitation, - ContentText, ContentToolRequestSearch, ContentToolResponseFetch, ContentToolResponseSearch, @@ -220,15 +219,13 @@ def test_google_web_search(): # Note: the cassette grounding chunks don't include a domain field, so # domain is None for all sources in this recording. cites = [ - cit + c for turn in chat.get_turns() for c in turn.contents - if isinstance(c, ContentText) - for cit in c.citations + if isinstance(c, ContentCitation) ] - assert cites - # Google supplies the grounded span directly (segment.text) - assert all(c.cited_text for c in cites) + assert cites, "expected ContentCitation items in turn contents" + assert all(c.url for c in cites) @pytest.mark.vcr @@ -246,7 +243,7 @@ def test_google_web_search_streaming(): assert results and results[0].sources citations = [x for x in items if isinstance(x, ContentCitation)] assert citations - assert all(c.citation.cited_text for c in citations) + assert all(c.url for c in citations) @pytest.mark.vcr diff --git a/tests/test_provider_openai.py b/tests/test_provider_openai.py index e8b296a8..a1f29a02 100644 --- a/tests/test_provider_openai.py +++ b/tests/test_provider_openai.py @@ -3,7 +3,7 @@ import httpx import pytest from chatlas import ChatOpenAI, tool_web_search -from chatlas.types import ContentCitation, ContentText, ContentToolRequestSearch +from chatlas.types import ContentCitation, ContentToolRequestSearch from openai.types.responses import ResponseOutputMessage, ResponseOutputText from .conftest import ( @@ -89,24 +89,15 @@ def chat_fun(**kwargs): hint="The CRAN archive page has this info.", ) - # Web search citations should be surfaced on ContentText (OpenAI provides offsets) + # Citations should be ContentCitation items in the turn contents cites = [ - cit + c for turn in chat.get_turns() - for content in turn.contents - if isinstance(content, ContentText) - for cit in content.citations + for c in turn.contents + if isinstance(c, ContentCitation) ] - assert cites, "expected url_citation annotations surfaced as citations" + assert cites, "expected ContentCitation items in turn contents" assert all(c.url for c in cites) - # cited_text is derived from the annotation offsets; every citation carries the span - answer = "".join( - content.text - for turn in chat.get_turns() - for content in turn.contents - if isinstance(content, ContentText) - ) - assert all(c.cited_text and c.cited_text in answer for c in cites) @pytest.mark.vcr @@ -121,9 +112,7 @@ def test_openai_web_search_streaming(): ) citations = [x for x in items if isinstance(x, ContentCitation)] assert citations - assert all(c.citation.url for c in citations) - answer = "".join(x for x in items if isinstance(x, str)) - assert all(c.citation.cited_text and c.citation.cited_text in answer for c in citations) + assert all(c.url for c in citations) # interleaved: at least one citation arrives before the last item in the stream cite_idx = [i for i, x in enumerate(items) if isinstance(x, ContentCitation)] assert cite_idx and min(cite_idx) < len(items) - 1 diff --git a/tests/test_turn_accumulator.py b/tests/test_turn_accumulator.py index e8fe9dbd..fa1a2f73 100644 --- a/tests/test_turn_accumulator.py +++ b/tests/test_turn_accumulator.py @@ -1,5 +1,4 @@ from chatlas._content import ( - Citation, ContentCitation, ContentText, ContentToolRequestSearch, @@ -42,8 +41,8 @@ def test_update_turn_appends_non_mergeable_adjacent_content(): acc.begin_turn(UserTurn("hello")) acc._update_turn(ContentToolRequestSearch(query="a")) acc._update_turn(ContentToolRequestSearch(query="b")) - acc._update_turn(ContentCitation(citation=Citation(url="https://a.com"))) - acc._update_turn(ContentCitation(citation=Citation(url="https://b.com"))) + acc._update_turn(ContentCitation(url="https://a.com")) + acc._update_turn(ContentCitation(url="https://b.com")) contents = turns[1].contents assert len(contents) == 4 assert [type(c).__name__ for c in contents] == [ @@ -123,7 +122,7 @@ def _acc() -> TurnAccumulator: def test_process_content_yields_citation_in_all_mode(): acc = _acc() - cit = ContentCitation(citation=Citation(url="https://a.com")) + cit = ContentCitation(url="https://a.com") out = list(acc.process_content(cit, None, "all", lambda x: None)) assert out == [cit] @@ -137,6 +136,6 @@ def test_process_content_yields_search_results_in_all_mode(): def test_process_content_text_mode_does_not_yield_citation(): acc = _acc() - cit = ContentCitation(citation=Citation(url="https://a.com")) + cit = ContentCitation(url="https://a.com") out = list(acc.process_content(cit, None, "text", lambda x: None)) assert out == [] From 172147eba02aa81fe0f619656c87ba2919ca4640 Mon Sep 17 00:00:00 2001 From: Carson Date: Fri, 12 Jun 2026 14:07:47 -0500 Subject: [PATCH 7/7] fix(docs): restore StreamController sidebar entry accidentally dropped --- docs/_sidebar.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/_sidebar.yml b/docs/_sidebar.yml index 210d52e4..5bba6ce0 100644 --- a/docs/_sidebar.yml +++ b/docs/_sidebar.yml @@ -26,6 +26,9 @@ website: - contents: - reference/Chat.qmd section: The chat object + - contents: + - reference/StreamController.qmd + section: Streaming - contents: - reference/content_image_file.qmd - reference/content_image_plot.qmd