diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py index 1950e37fc80..92adf407121 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/streaming_iterator.py @@ -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", diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py index 1d25d719384..81f1126c954 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py @@ -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"}