From 8add0d1e52a639ada17f79cb137310c0695f8bb1 Mon Sep 17 00:00:00 2001 From: Animesh Kumar Date: Fri, 11 Sep 2026 13:18:33 +0530 Subject: [PATCH] fix(anthropic): tolerate a text block start without the text key api.anthropic.com always sends "text": "" on a text content_block_start, so chunk_parser indexed it directly. At least one Anthropic-compatible gateway omits the key, and the first content block of every streamed response then raised KeyError and killed the stream. Absence there has one possible meaning, so read it with a default rather than dropping the response. The surrounding branches already read their optional fields the same way. Fixes #40689 --- litellm/llms/anthropic/chat/handler.py | 5 ++- .../chat/test_anthropic_chat_handler.py | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index d1fe4cadf40..98210a1d5a7 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -891,7 +891,10 @@ 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"] + # Anthropic always sends "text": "" here, but some compatible gateways omit + # the key. Absence on a text block start has the same meaning as the empty + # string, so read it leniently rather than dropping the stream. + text = content_block_start["content_block"].get("text", "") elif ( content_block_start["content_block"]["type"] == "tool_use" or content_block_start["content_block"]["type"] == "server_tool_use" diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py index 18309595414..43bc733d8e8 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py @@ -532,6 +532,42 @@ def test_regular_tool_finish_reason(): assert model_response.choices[0].finish_reason == "tool_calls" +def test_content_block_start_without_text_key_does_not_kill_the_stream(): + """Some Anthropic-compatible gateways omit "text" on a text block start. + + api.anthropic.com always sends "text": "", so absence carries the same meaning. + https://github.com/BerriAI/litellm/issues/40689 + """ + chunks = [ + { + "type": "message_start", + "message": { + "id": "msg_123", + "type": "message", + "role": "assistant", + "content": [], + "usage": {"input_tokens": 10, "output_tokens": 1}, + }, + }, + # no "text" key, unlike the official API + {"type": "content_block_start", "index": 0, "content_block": {"type": "text"}}, + { + "type": "content_block_delta", + "index": 0, + "delta": {"type": "text_delta", "text": "hi"}, + }, + {"type": "content_block_stop", "index": 0}, + ] + + iterator = ModelResponseIterator(None, sync_stream=True) + parsed = [iterator.chunk_parser(chunk) for chunk in chunks] + + start = parsed[1] + assert start.choices[0].delta.content == "" + delta = parsed[2] + assert delta.choices[0].delta.content == "hi" + + def test_text_only_streaming_has_index_zero(): """Test that text-only streaming responses have choice index=0""" chunks = [