mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
Merge e3fb8b1f1f into a6816f0e96
This commit is contained in:
commit
fe31c46f51
2 changed files with 67 additions and 27 deletions
|
|
@ -1470,8 +1470,8 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
return "tool_use", cast("ContentBlockContentBlockDict", tool_block)
|
||||
elif choice.delta.content is not None and len(choice.delta.content) > 0:
|
||||
return "text", TextBlock(type="text", text="")
|
||||
elif isinstance(choice, StreamingChoices) and hasattr(choice.delta, "thinking_blocks"):
|
||||
thinking_blocks = choice.delta.thinking_blocks or []
|
||||
elif isinstance(choice, StreamingChoices):
|
||||
thinking_blocks = getattr(choice.delta, "thinking_blocks", None) or []
|
||||
if len(thinking_blocks) > 0:
|
||||
thinking_block = thinking_blocks[0]
|
||||
if thinking_block["type"] == "thinking":
|
||||
|
|
@ -1484,16 +1484,33 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
return "thinking", ChatCompletionThinkingBlock(
|
||||
type="thinking", thinking=thinking, signature=signature
|
||||
)
|
||||
# OpenAI-compatible reasoning backends (e.g. vLLM/SGLang reasoning
|
||||
# parsers) populate ``reasoning_content`` without ``thinking_blocks``.
|
||||
# ``Delta`` deletes the ``thinking_blocks`` attribute when unset, so the
|
||||
# branch above is skipped entirely; open a ``thinking`` block here so the
|
||||
# matching ``thinking_delta`` stream is not emitted into a text block.
|
||||
elif isinstance(choice, StreamingChoices) and getattr(choice.delta, "reasoning_content", None):
|
||||
return "thinking", ChatCompletionThinkingBlock(type="thinking", thinking="", signature="")
|
||||
if getattr(choice.delta, "reasoning_content", None):
|
||||
return "thinking", ChatCompletionThinkingBlock(type="thinking", thinking="", signature="")
|
||||
|
||||
return "text", TextBlock(type="text", text="")
|
||||
|
||||
@staticmethod
|
||||
def _streaming_reasoning_fields(choice: StreamingChoices) -> tuple[str, str]:
|
||||
reasoning_content = ""
|
||||
reasoning_signature = ""
|
||||
thinking_blocks = getattr(choice.delta, "thinking_blocks", None) or []
|
||||
for thinking_block in thinking_blocks:
|
||||
if thinking_block["type"] == "thinking":
|
||||
thinking = thinking_block.get("thinking") or ""
|
||||
signature = thinking_block.get("signature") or ""
|
||||
|
||||
assert isinstance(thinking, str)
|
||||
assert isinstance(signature, str)
|
||||
|
||||
reasoning_content += thinking
|
||||
reasoning_signature += signature
|
||||
|
||||
if reasoning_content or reasoning_signature:
|
||||
return reasoning_content, reasoning_signature
|
||||
|
||||
fallback = getattr(choice.delta, "reasoning_content", None)
|
||||
return fallback or "", ""
|
||||
|
||||
def _translate_streaming_openai_chunk_to_anthropic(
|
||||
self, choices: list[OpenAIStreamingChoice | StreamingChoices]
|
||||
) -> tuple[
|
||||
|
|
@ -1512,24 +1529,10 @@ class LiteLLMAnthropicMessagesAdapter:
|
|||
for tool in choice.delta.tool_calls:
|
||||
if tool.function is not None and tool.function.arguments is not None:
|
||||
partial_json = (partial_json or "") + tool.function.arguments
|
||||
elif isinstance(choice, StreamingChoices) and hasattr(choice.delta, "thinking_blocks"):
|
||||
thinking_blocks = choice.delta.thinking_blocks or []
|
||||
if len(thinking_blocks) > 0:
|
||||
for thinking_block in thinking_blocks:
|
||||
if thinking_block["type"] == "thinking":
|
||||
thinking = thinking_block.get("thinking") or ""
|
||||
signature = thinking_block.get("signature") or ""
|
||||
|
||||
assert isinstance(thinking, str)
|
||||
assert isinstance(signature, str)
|
||||
|
||||
reasoning_content += thinking
|
||||
reasoning_signature += signature
|
||||
# Handle reasoning_content when thinking_blocks is not present
|
||||
# This handles providers like OpenRouter that return reasoning_content
|
||||
elif isinstance(choice, StreamingChoices) and hasattr(choice.delta, "reasoning_content"):
|
||||
if choice.delta.reasoning_content is not None:
|
||||
reasoning_content += choice.delta.reasoning_content
|
||||
elif isinstance(choice, StreamingChoices):
|
||||
choice_reasoning_content, choice_reasoning_signature = self._streaming_reasoning_fields(choice)
|
||||
reasoning_content += choice_reasoning_content
|
||||
reasoning_signature += choice_reasoning_signature
|
||||
|
||||
if partial_json is not None:
|
||||
return "input_json_delta", ContentJsonBlockDelta(type="input_json_delta", partial_json=partial_json)
|
||||
|
|
|
|||
|
|
@ -573,6 +573,43 @@ def test_reasoning_content_first_stream_opens_thinking_block_at_index_zero_sync(
|
|||
_assert_thinking_first_block_opens_at_index_zero(_drain_sync(wrapper))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"thinking_blocks",
|
||||
[
|
||||
pytest.param([], id="empty"),
|
||||
pytest.param(
|
||||
[{"type": "redacted_thinking", "data": "redacted"}],
|
||||
id="redacted-only",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_reasoning_content_falls_back_without_usable_thinking_blocks_sync(
|
||||
thinking_blocks,
|
||||
):
|
||||
chunks = [
|
||||
_make_chunk(
|
||||
Delta(
|
||||
content=None,
|
||||
reasoning_content="fallback thought",
|
||||
thinking_blocks=thinking_blocks,
|
||||
)
|
||||
),
|
||||
_make_chunk(Delta(content=None), finish_reason="stop"),
|
||||
]
|
||||
wrapper = AnthropicStreamWrapper(completion_stream=iter(chunks), model="claude-x")
|
||||
events = _drain_sync(wrapper)
|
||||
|
||||
starts = [
|
||||
(event["index"], event["content_block"]["type"])
|
||||
for event in events
|
||||
if event.get("type") == "content_block_start"
|
||||
]
|
||||
assert starts == [(0, "thinking")]
|
||||
assert _thinking_deltas(events) == ["fallback thought"]
|
||||
assert _text_deltas(events) == []
|
||||
_assert_deltas_match_their_block_type(events)
|
||||
|
||||
|
||||
def _blank_lead_chunks() -> List[MagicMock]:
|
||||
return [
|
||||
_make_chunk(Delta(content=None)),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue