From bbda70edd5b91db79eb94a36282796e486145b49 Mon Sep 17 00:00:00 2001 From: Zakaria Khchiche <45357915+Zakariakhchiche@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:40:35 +0200 Subject: [PATCH 1/3] Python: Surface Anthropic citations_delta annotations when streaming --- .../agent_framework_anthropic/_chat_client.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py index 5cc91b1efed..0d6dfabacb0 100644 --- a/python/packages/anthropic/agent_framework_anthropic/_chat_client.py +++ b/python/packages/anthropic/agent_framework_anthropic/_chat_client.py @@ -1366,6 +1366,16 @@ def _parse_contents_from_anthropic( annotations=self._parse_citations_from_anthropic(content_block), ) ) + case "citations_delta": + # Streaming delivers citations as separate deltas on the text block; + # surface them as an empty text carrying the annotation so they merge into it. + contents.append( + Content.from_text( + text="", + raw_representation=content_block, + annotations=self._parse_citations_from_anthropic(content_block), + ) + ) case "tool_use" | "mcp_tool_use" | "server_tool_use": request_state.active_call_id = content_block.id request_state.active_call_content_type = content_block.type @@ -1664,7 +1674,9 @@ def _parse_contents_from_anthropic( def _parse_citations_from_anthropic( self, content_block: BetaContentBlock | BetaRawContentBlockDelta | BetaTextBlock ) -> list[Annotation] | None: - content_blocks = getattr(content_block, "citations", None) + content_blocks: Any = getattr(content_block, "citations", None) + if content_block.type == "citations_delta": + content_blocks = [content_block.citation] if not content_blocks: return None annotations: list[Annotation] = [] From 288b52aeedcaf64a368c1506a50d96fa523a69a8 Mon Sep 17 00:00:00 2001 From: Zakaria Khchiche <45357915+Zakariakhchiche@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:41:07 +0200 Subject: [PATCH 2/3] Add streaming citations_delta regression test for AnthropicClient --- .../anthropic/tests/test_anthropic_client.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 0b330a469cb..96281ecc091 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -36,9 +36,14 @@ ) from agent_framework.observability import ChatTelemetryLayer from anthropic.types.beta import ( + BetaCitationsDelta, + BetaCitationsWebSearchResultLocation, BetaMessage, BetaMessageDeltaUsage, + BetaRawContentBlockDeltaEvent, + BetaRawContentBlockStartEvent, BetaTextBlock, + BetaTextDelta, BetaToolUseBlock, BetaUsage, ) @@ -4194,6 +4199,54 @@ def test_parse_citations_search_result_location( assert len(result) > 0 +async def test_streaming_citations_delta_surfaces_annotations(mock_anthropic_client: MagicMock) -> None: + """Citations streamed as citations_delta events must end up as annotations, like non-streaming.""" + citation = BetaCitationsWebSearchResultLocation( + type="web_search_result_location", + url="https://example.com/paris", + title="Paris weather", + cited_text="Paris is sunny today.", + encrypted_index="enc-1", + ) + + async def events() -> Any: + yield _message_start_event("msg_cited") + yield BetaRawContentBlockStartEvent( + type="content_block_start", index=0, content_block=BetaTextBlock(type="text", text="", citations=None) + ) + yield BetaRawContentBlockDeltaEvent( + type="content_block_delta", index=0, delta=BetaCitationsDelta(type="citations_delta", citation=citation) + ) + yield BetaRawContentBlockDeltaEvent( + type="content_block_delta", index=0, delta=BetaTextDelta(type="text_delta", text="It is sunny in Paris.") + ) + yield _content_block_stop_event() + yield _message_delta_event("end_turn") + yield _message_stop_event() + + async def create(**kwargs: Any) -> Any: + return events() + + client = create_test_anthropic_client(mock_anthropic_client) + mock_anthropic_client.beta.messages.create.side_effect = create + stream = client.get_response([Message(role="user", contents=["Weather in Paris?"])], stream=True) + _ = [update async for update in stream] + response = await stream.get_final_response() + + assert response.text == "It is sunny in Paris." + annotations = [ + annotation + for message in response.messages + for content in message.contents + if content.type == "text" + for annotation in content.annotations or [] + ] + assert len(annotations) == 1 + assert annotations[0]["url"] == "https://example.com/paris" + assert annotations[0]["title"] == "Paris weather" + assert annotations[0]["snippet"] == "Paris is sunny today." + + @pytest.mark.flaky @pytest.mark.integration @skip_if_anthropic_integration_tests_disabled From f34f3e591f27fba07db1d84937211c1ea3c0cced Mon Sep 17 00:00:00 2001 From: Zakaria Khchiche <45357915+Zakariakhchiche@users.noreply.github.com> Date: Fri, 2 Oct 2026 19:11:28 +0200 Subject: [PATCH 3/3] Iterate the stream directly in the citations_delta test --- python/packages/anthropic/tests/test_anthropic_client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/packages/anthropic/tests/test_anthropic_client.py b/python/packages/anthropic/tests/test_anthropic_client.py index 96281ecc091..72fcb0c4888 100644 --- a/python/packages/anthropic/tests/test_anthropic_client.py +++ b/python/packages/anthropic/tests/test_anthropic_client.py @@ -4230,7 +4230,8 @@ async def create(**kwargs: Any) -> Any: client = create_test_anthropic_client(mock_anthropic_client) mock_anthropic_client.beta.messages.create.side_effect = create stream = client.get_response([Message(role="user", contents=["Weather in Paris?"])], stream=True) - _ = [update async for update in stream] + async for _ in stream: + pass response = await stream.get_final_response() assert response.text == "It is sunny in Paris."