mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
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:
parent
9a715df212
commit
8add0d1e52
2 changed files with 40 additions and 1 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue