Fix AnthropicStreamWrapper: correct content_block_start type + suppress empty input_json_delta

- Peek at first chunk before emitting content_block_start so the block
  type (thinking, tool_use, text) is correct instead of always hardcoding text
- Suppress spurious empty input_json_delta that was produced when translating
  the OpenAI-style tool_use start chunk (arguments="") and being held as
  holding_chunk
- Update test_parallel_tool_calls.py to reflect the new correct behaviour
  (no initial empty text block before tool_use blocks); the TODO comments
  in those tests explicitly anticipated this fix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
tko 2026-04-30 04:29:35 +00:00
parent df7148036a
commit f92c73dbc7
2 changed files with 26 additions and 23 deletions

View file

@ -115,10 +115,23 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper):
"AnthropicStreamWrapper: stream yielded None as first chunk"
)
self._should_start_new_content_block(first_chunk)
self.holding_chunk = LiteLLMAnthropicMessagesAdapter().translate_streaming_openai_response_to_anthropic(
_translated = LiteLLMAnthropicMessagesAdapter().translate_streaming_openai_response_to_anthropic(
response=first_chunk,
current_content_block_index=self.current_content_block_index,
)
# Don't hold an empty input_json_delta: OpenAI-style tool_use
# start chunks carry arguments="" which translates to a
# partial_json="" delta that should not be emitted.
if (
isinstance(_translated, dict)
and _translated.get("type") == "content_block_delta"
and isinstance(_translated.get("delta"), dict)
and _translated["delta"].get("type") == "input_json_delta"
and not _translated["delta"].get("partial_json")
):
self.holding_chunk = None
else:
self.holding_chunk = _translated
self.chunk_queue.append(
{
"type": "content_block_start",
@ -273,10 +286,21 @@ class AnthropicStreamWrapper(AdapterCompletionStreamWrapper):
"AnthropicStreamWrapper: stream yielded None as first chunk"
)
self._should_start_new_content_block(first_chunk)
self.holding_chunk = LiteLLMAnthropicMessagesAdapter().translate_streaming_openai_response_to_anthropic(
_translated = LiteLLMAnthropicMessagesAdapter().translate_streaming_openai_response_to_anthropic(
response=first_chunk,
current_content_block_index=self.current_content_block_index,
)
# Don't hold an empty input_json_delta (see __next__ for explanation)
if (
isinstance(_translated, dict)
and _translated.get("type") == "content_block_delta"
and isinstance(_translated.get("delta"), dict)
and _translated["delta"].get("type") == "input_json_delta"
and not _translated["delta"].get("partial_json")
):
self.holding_chunk = None
else:
self.holding_chunk = _translated
self.chunk_queue.append(
{
"type": "content_block_start",

View file

@ -134,13 +134,6 @@ def test_anthropic_stream_wrapper_single_tool_call():
# Verify the expected sequence of chunk types
expected_types = [
"message_start", # Initial message start
# TODO: for future contributors: if the initial content_block_start
# respects the upstream's starting chunk, the initial empty text block
# should be removed (and this test should be updated accordingly)
# ---------------------------------------------------------------------
"content_block_start", # Initial empty text block start
"content_block_stop", # End of empty text block
# ---------------------------------------------------------------------
"content_block_start", # Start of first tool_use content block
"content_block_delta", # {"city":
"content_block_delta", # "NY"}
@ -196,13 +189,6 @@ def test_anthropic_stream_wrapper_back_to_back_tool_calls():
# Verify the expected sequence of chunk types
expected_types = [
"message_start", # Initial message start
# TODO: for future contributors: if the initial content_block_start
# respects the upstream's starting chunk, the initial empty text block
# should be removed (and this test should be updated accordingly)
# ---------------------------------------------------------------------
"content_block_start", # Initial empty text block start
"content_block_stop", # End of empty text block
# ---------------------------------------------------------------------
"content_block_start", # Start of first tool_use content block
"content_block_delta", # {"city":
"content_block_delta", # "NY"}
@ -267,13 +253,6 @@ def test_anthropic_stream_wrapper_interleaved_tool_calls_and_text():
# Verify the expected sequence of chunk types
expected_types = [
"message_start", # Initial message start
# TODO: for future contributors: if the initial content_block_start
# respects the upstream's starting chunk, the initial empty text block
# should be removed (and this test should be updated accordingly)
# ---------------------------------------------------------------------
"content_block_start", # Initial empty text block start
"content_block_stop", # End of empty text block
# ---------------------------------------------------------------------
"content_block_start", # Start of first tool_use content block
"content_block_delta", # {"city":
"content_block_delta", # "NY"}