mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-27 06:13:07 +00:00
fix(anthropic): open unsigned thinking blocks with an empty body in /v1/messages streams
This commit is contained in:
parent
4e2dd65549
commit
34f5c5c8bf
3 changed files with 27 additions and 2 deletions
|
|
@ -1671,8 +1671,11 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
assert isinstance(thinking, str)
|
||||
assert isinstance(signature, str)
|
||||
|
||||
# Without a signature the same text arrives again as a
|
||||
# thinking_delta, so an SSE accumulator seeding from this
|
||||
# start body would show it twice.
|
||||
return "thinking", ChatCompletionThinkingBlock(
|
||||
type="thinking", thinking=thinking, signature=signature
|
||||
type="thinking", thinking=thinking if signature else "", signature=signature
|
||||
)
|
||||
# OpenAI-compatible reasoning backends (e.g. vLLM/SGLang reasoning
|
||||
# parsers) populate ``reasoning_content`` without ``thinking_blocks``.
|
||||
|
|
|
|||
|
|
@ -285,9 +285,10 @@ def test_translate_streaming_openai_chunk_to_anthropic_thinking_content_block():
|
|||
) = LiteLLMAnthropicMessagesAdapter().translate_streaming_openai_chunk_to_anthropic_content_block(choices=choices)
|
||||
|
||||
assert block_type == "thinking"
|
||||
# Unsigned: the text is delivered by the thinking_delta, so the start body stays empty.
|
||||
assert content_block_start == {
|
||||
"type": "thinking",
|
||||
"thinking": "I need to summar",
|
||||
"thinking": "",
|
||||
"signature": "",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -948,6 +948,27 @@ def test_mixed_thinking_start_body_is_empty_and_thinking_not_doubled_sync():
|
|||
assert _text_deltas(events) == ["Answer."]
|
||||
|
||||
|
||||
def test_pure_thinking_start_body_is_empty_and_thinking_not_doubled_sync():
|
||||
chunks = [
|
||||
_thinking_chunk("Hello"),
|
||||
_thinking_chunk(" world"),
|
||||
_make_chunk(Delta(content="answer")),
|
||||
_make_chunk(Delta(content=None), finish_reason="stop"),
|
||||
]
|
||||
wrapper = AnthropicStreamWrapper(completion_stream=iter(chunks), model="claude-x")
|
||||
events = _drain_sync(wrapper)
|
||||
|
||||
accumulated = ""
|
||||
for event in events:
|
||||
if event.get("type") == "content_block_start" and event["content_block"].get("type") == "thinking":
|
||||
assert not event["content_block"].get("thinking"), event["content_block"]
|
||||
accumulated += event["content_block"].get("thinking") or ""
|
||||
if event.get("type") == "content_block_delta" and event["delta"].get("type") == "thinking_delta":
|
||||
accumulated += event["delta"]["thinking"]
|
||||
assert accumulated == "Hello world"
|
||||
assert _text_deltas(events) == ["answer"]
|
||||
|
||||
|
||||
def test_mixed_chunk_with_tool_argument_continuation_is_not_split_sync():
|
||||
"""Streaming providers send a tool call's name only on its first chunk;
|
||||
later chunks carry argument fragments with ``name=None``. Splitting a
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue