fix(anthropic): open unsigned thinking blocks with an empty body in /v1/messages streams

This commit is contained in:
Zhu Lei 2026-10-16 21:37:44 +08:00
parent 4e2dd65549
commit 34f5c5c8bf
3 changed files with 27 additions and 2 deletions

View file

@ -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``.

View file

@ -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": "",
}

View file

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