fix(anthropic): preserve reasoning fallback

This commit is contained in:
jesco-absolut 2026-07-31 23:20:45 -04:00
parent 23de7a15d9
commit 0d7217ce58
2 changed files with 53 additions and 18 deletions

View file

@ -1407,8 +1407,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":
@ -1421,13 +1421,8 @@ 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="")
@ -1454,8 +1449,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 []
elif isinstance(choice, StreamingChoices):
thinking_blocks = getattr(choice.delta, "thinking_blocks", None) or []
choice_reasoning_content = ""
choice_reasoning_signature = ""
if len(thinking_blocks) > 0:
for thinking_block in thinking_blocks:
if thinking_block["type"] == "thinking":
@ -1465,13 +1462,14 @@ class LiteLLMAnthropicMessagesAdapter:
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
choice_reasoning_content += thinking
choice_reasoning_signature += signature
if choice_reasoning_content or choice_reasoning_signature:
reasoning_content += choice_reasoning_content
reasoning_signature += choice_reasoning_signature
elif hasattr(choice.delta, "reasoning_content"):
if choice.delta.reasoning_content is not None:
reasoning_content += choice.delta.reasoning_content
if partial_json is not None:
return "input_json_delta", ContentJsonBlockDelta(type="input_json_delta", partial_json=partial_json)

View file

@ -576,6 +576,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)),