From 91c77e4d73af4abddd5a5af8b79cfe2794b8f26b Mon Sep 17 00:00:00 2001 From: mahenoorsalat Date: Fri, 11 Sep 2026 13:18:24 +0530 Subject: [PATCH] fix(anthropic): handle omitted text field in content_block_start (#40689) --- litellm/llms/anthropic/chat/handler.py | 2 +- litellm/types/llms/anthropic.py | 2 +- ...anthropic_streaming_content_block_start.py | 51 +++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 tests/test_anthropic_streaming_content_block_start.py diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index d1fe4cadf40..c7f7e2273af 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -891,7 +891,7 @@ class ModelResponseIterator: # Track current content block type for filtering deltas self.current_content_block_type = content_block_start["content_block"]["type"] if content_block_start["content_block"]["type"] == "text": - text = content_block_start["content_block"]["text"] + text = content_block_start["content_block"].get("text") or "" elif ( content_block_start["content_block"]["type"] == "tool_use" or content_block_start["content_block"]["type"] == "server_tool_use" diff --git a/litellm/types/llms/anthropic.py b/litellm/types/llms/anthropic.py index 365d59a179b..6c64285225c 100644 --- a/litellm/types/llms/anthropic.py +++ b/litellm/types/llms/anthropic.py @@ -495,7 +495,7 @@ class ToolUseBlock(TypedDict): caller: ToolCaller | None -class TextBlock(TypedDict): +class TextBlock(TypedDict, total=False): text: str type: Literal["text"] diff --git a/tests/test_anthropic_streaming_content_block_start.py b/tests/test_anthropic_streaming_content_block_start.py new file mode 100644 index 00000000000..49c39a9307d --- /dev/null +++ b/tests/test_anthropic_streaming_content_block_start.py @@ -0,0 +1,51 @@ +import pytest +from litellm.llms.anthropic.chat.handler import ModelResponseIterator + + +def test_chunk_parser_content_block_start_omits_text(): + """ + Test that ModelResponseIterator.chunk_parser handles content_block_start + when the 'text' key is omitted by third-party Anthropic-compatible upstreams. + Fixes Issue #40689. + """ + iterator = ModelResponseIterator(streaming_response=[], sync_stream=True) + + # 1. Chunk omitting "text" entirely + chunk_without_text = { + "type": "content_block_start", + "index": 0, + "content_block": {"type": "text"}, + } + res = iterator.chunk_parser(chunk_without_text) + assert res is not None + assert res.choices[0].delta.content == "" + + # 2. Chunk with standard official empty string text + chunk_with_empty_text = { + "type": "content_block_start", + "index": 0, + "content_block": {"type": "text", "text": ""}, + } + res = iterator.chunk_parser(chunk_with_empty_text) + assert res is not None + assert res.choices[0].delta.content == "" + + # 3. Chunk with text as None + chunk_with_none_text = { + "type": "content_block_start", + "index": 0, + "content_block": {"type": "text", "text": None}, + } + res = iterator.chunk_parser(chunk_with_none_text) + assert res is not None + assert res.choices[0].delta.content == "" + + # 4. Chunk with actual initial text + chunk_with_text = { + "type": "content_block_start", + "index": 0, + "content_block": {"type": "text", "text": "hello"}, + } + res = iterator.chunk_parser(chunk_with_text) + assert res is not None + assert res.choices[0].delta.content == "hello"