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
This commit is contained in:
Animesh Kumar 2026-09-11 13:18:33 +05:30
parent 9a715df212
commit 8add0d1e52
2 changed files with 40 additions and 1 deletions

View file

@ -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"

View file

@ -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 = [