diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index a5eff2aa17dd..4c202b9eec0d 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -613,7 +613,14 @@ def _translate_openai_content_to_anthropic(self, choices: List[Choices]) -> List ) ) - # Handle tool calls + # Handle text content + if choice.message.content is not None: + new_content.append( + AnthropicResponseContentBlockText( + type="text", text=choice.message.content + ) + ) + # Handle tool calls (in parallel to text content) if ( choice.message.tool_calls is not None and len(choice.message.tool_calls) > 0 @@ -642,13 +649,6 @@ def _translate_openai_content_to_anthropic(self, choices: List[Choices]) -> List provider_specific_fields ) new_content.append(tool_use_block) - # Handle text content - elif choice.message.content is not None: - new_content.append( - AnthropicResponseContentBlockText( - type="text", text=choice.message.content - ) - ) return new_content diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py index c4b94481dfdd..9d6fbf66e480 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py @@ -1,5 +1,6 @@ import os import sys +from typing import Any, cast import pytest @@ -20,7 +21,9 @@ Delta, Function, Message, + ModelResponse, StreamingChoices, + Usage, ) @@ -341,6 +344,81 @@ def test_translate_openai_content_to_anthropic_empty_function_arguments(): assert result[0].input == {}, "Empty function arguments should result in empty dict" +def test_translate_openai_content_to_anthropic_text_and_tool_calls(): + """Ensure content blocks contain both the assistant text + tool call data.""" + openai_choices = [ + Choices( + message=Message( + role="assistant", + content="Calling get_weather now.", + tool_calls=[ + ChatCompletionAssistantToolCall( + id="call_weather", + type="function", + function=Function( + name="get_weather", + arguments='{"location": "Boston"}', + ), + ) + ], + ) + ) + ] + + adapter = LiteLLMAnthropicMessagesAdapter() + result = adapter._translate_openai_content_to_anthropic(choices=openai_choices) + + assert len(result) == 2 + assert result[0].type == "text" + assert result[0].text == "Calling get_weather now." + assert result[1].type == "tool_use" + assert result[1].id == "call_weather" + assert result[1].name == "get_weather" + assert result[1].input == {"location": "Boston"} + + +def test_translate_openai_response_to_anthropic_text_and_tool_calls(): + """`translate_openai_response_to_anthropic` should surface assistant text even when tools fire.""" + openai_response = ModelResponse( + id="resp_text_tool", + model="gpt-4o-mini", + choices=[ + Choices( + finish_reason="tool_calls", + message=Message( + role="assistant", + content="Let me grab the current weather.", + tool_calls=[ + ChatCompletionAssistantToolCall( + id="call_tool_combo", + type="function", + function=Function( + name="get_weather", arguments='{"location": "Paris"}' + ), + ) + ], + ), + ) + ], + usage=Usage(prompt_tokens=5, completion_tokens=2), + ) + + adapter = LiteLLMAnthropicMessagesAdapter() + anthropic_response = adapter.translate_openai_response_to_anthropic( + response=openai_response + ) + + anthropic_content = anthropic_response.get("content") + assert anthropic_content is not None + assert len(anthropic_content) == 2 + assert cast(Any, anthropic_content[0]).type == "text" + assert cast(Any, anthropic_content[0]).text == "Let me grab the current weather." + assert cast(Any, anthropic_content[1]).type == "tool_use" + assert cast(Any, anthropic_content[1]).id == "call_tool_combo" + assert cast(Any, anthropic_content[1]).input == {"location": "Paris"} + assert anthropic_response.get("stop_reason") == "tool_use" + + def test_translate_streaming_openai_chunk_to_anthropic_with_partial_json(): """Test that partial tool arguments are correctly handled as input_json_delta.""" choices = [