diff --git a/ddtrace/contrib/internal/anthropic/_streaming.py b/ddtrace/contrib/internal/anthropic/_streaming.py index 5de5fd0c7b3..73ec8404b83 100644 --- a/ddtrace/contrib/internal/anthropic/_streaming.py +++ b/ddtrace/contrib/internal/anthropic/_streaming.py @@ -165,6 +165,12 @@ def _on_content_block_delta_chunk(chunk, message): def _on_content_block_stop_chunk(chunk, message): # this is the start to a message.content block (possibly 1 of several content blocks) + # Anthropic beta streaming can emit content_block_stop without a corresponding + # content_block_start (e.g. empty tool blocks / vendor edge cases). Guard to + # avoid IndexError which breaks span construction. + if not message.get("content"): + return message + content_type = _get_attr(message["content"][-1], "type", "") if content_type == "tool_use": input_json = _get_attr(message["content"][-1], "input", "{}") diff --git a/tests/contrib/anthropic/test_anthropic_llmobs.py b/tests/contrib/anthropic/test_anthropic_llmobs.py index b036a359a45..97c8b6361bc 100644 --- a/tests/contrib/anthropic/test_anthropic_llmobs.py +++ b/tests/contrib/anthropic/test_anthropic_llmobs.py @@ -61,6 +61,13 @@ ], ) class TestLLMObsAnthropic: + def test_content_block_stop_without_content_does_not_crash(self): + """Regression test for beta streaming: content_block_stop can arrive without any content blocks.""" + from ddtrace.contrib.internal.anthropic._streaming import _on_content_block_stop_chunk + + message = {"content": []} + assert _on_content_block_stop_chunk(chunk=None, message=message) == message + @patch("anthropic._base_client.SyncAPIClient.post") def test_completion_proxy( self,