From 9befaab2883f393a90efd84450c40649350a80cc Mon Sep 17 00:00:00 2001 From: Qisthi Ramadhani Date: Sat, 16 May 2026 22:24:27 +0700 Subject: [PATCH] Fix KeyError 'text' on content_block_start in Anthropic streaming Some Anthropic-compatible upstream providers (e.g. api-cc.freemodel.dev proxying Claude Haiku 4.5) emit content_block_start events for text blocks without the optional 'text' field, causing KeyError in the chunk_parser. Per Anthropic's streaming spec the initial text on content_block_start is conventionally empty (""), and the actual content arrives via subsequent content_block_delta chunks regardless. Use .get("text", "") so the parser tolerates the missing field instead of aborting the stream with MidStreamFallbackError. Reproduction: curl -sN -X POST http://proxy/v1/chat/completions \ -H 'Authorization: Bearer $KEY' \ -d '{"model":"claude-haiku-4-5","messages":[...],"stream":true}' Before patch: stream aborts with KeyError: 'text' at handler.py:789 After patch: stream completes normally. --- litellm/llms/anthropic/chat/handler.py | 7 ++++- .../chat/test_anthropic_chat_handler.py | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index 2fb29b32a61..8d9225f0767 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -820,7 +820,12 @@ class ModelResponseIterator: "type" ] if content_block_start["content_block"]["type"] == "text": - text = content_block_start["content_block"]["text"] + # Defensive .get() — some upstream Anthropic-compatible + # providers omit the "text" field on content_block_start + # for "text" blocks (spec says it should be ""). Fall back + # to "" instead of raising KeyError, since the actual text + # arrives via subsequent content_block_delta chunks anyway. + 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 2fdd639e74d..ed6aeda810e 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 @@ -1901,3 +1901,30 @@ def test_non_bash_tool_result_skipped(): assert ( len(code_results) == 0 ), f"Expected 0 code_interpreter_results for text_editor result, got {len(code_results)}" + + +def test_content_block_start_text_missing_text_field(): + """ + Regression test: some Anthropic-compatible upstreams omit the optional + "text" field on content_block_start events whose type == "text". Per + the Anthropic streaming spec the value is conventionally "", and the + real text arrives via subsequent content_block_delta chunks anyway, + so the parser must tolerate the missing field instead of raising + KeyError mid-stream. + """ + chunk = { + "type": "content_block_start", + "index": 0, + # NOTE: "text" field intentionally omitted to mirror upstream payloads + # observed from Anthropic-compatible providers (e.g. proxies fronting + # claude-haiku-4-5-20251001) that skip the empty default. + "content_block": {"type": "text"}, + } + + iterator = ModelResponseIterator(None, sync_stream=True) + + # Must not raise KeyError: 'text' + parsed = iterator.chunk_parser(chunk=chunk) + + assert parsed is not None + assert parsed.choices[0].delta.content == ""