diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index d7b527824ea..1d86c105e61 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -455,6 +455,14 @@ class LiteLLMAnthropicMessagesAdapter: ) self._add_cache_control_if_applicable(content, tool_result, model) tool_message_list.append(tool_result) + elif c.get("type") == "tool_reference": + tool_result = ChatCompletionToolMessage( + role="tool", + tool_call_id=content.get("tool_use_id", ""), + content=f"[Loaded tool: {c.get('tool_name', '')}]", + ) + self._add_cache_control_if_applicable(content, tool_result, model) + tool_message_list.append(tool_result) elif c.get("type") in ("image", "document"): image_part = self._tool_result_image_part(c.get("source")) tool_result = ChatCompletionToolMessage( @@ -483,6 +491,13 @@ class LiteLLMAnthropicMessagesAdapter: text=c.get("text", ""), ) ) + elif c.get("type") == "tool_reference": + combined_content_parts.append( + ChatCompletionTextObject( + type="text", + text=f"[Loaded tool: {c.get('tool_name', '')}]", + ) + ) elif c.get("type") in ("image", "document"): image_part = self._tool_result_image_part(c.get("source")) if image_part: diff --git a/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py index ace7fc25dc9..f392c5335dd 100644 --- a/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py @@ -294,7 +294,11 @@ class LiteLLMAnthropicToResponsesAPIAdapter: output_text = inner elif isinstance(inner, list): parts = [ - c.get("text", "") for c in inner if isinstance(c, dict) and c.get("type") == "text" + c.get("text", "") + if c.get("type") == "text" + else f"[Loaded tool: {c.get('tool_name', '')}]" + for c in inner + if isinstance(c, dict) and c.get("type") in {"text", "tool_reference"} ] output_text = "\n".join(parts) image_candidates = tuple( diff --git a/litellm/types/llms/anthropic.py b/litellm/types/llms/anthropic.py index 901802a6640..7805dd595a2 100644 --- a/litellm/types/llms/anthropic.py +++ b/litellm/types/llms/anthropic.py @@ -324,7 +324,12 @@ class AnthropicMessagesToolResultParam(TypedDict, total=False): is_error: bool content: ( str - | Iterable[AnthropicMessagesToolResultContent | AnthropicMessagesImageParam | AnthropicMessagesDocumentParam] + | Iterable[ + AnthropicMessagesToolResultContent + | AnthropicMessagesImageParam + | AnthropicMessagesDocumentParam + | ToolReference + ] ) cache_control: dict | ChatCompletionCachedContent | None 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 95fbd06547b..8bc865181df 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 @@ -1610,6 +1610,70 @@ def test_translate_anthropic_messages_to_openai_tool_result_single_item_backward assert tool_message["content"] == "72°F and sunny" +@pytest.mark.parametrize( + ("tool_references", "expected_content"), + [ + ( + [{"type": "tool_reference", "tool_name": "get_weather"}], + "[Loaded tool: get_weather]", + ), + ( + [ + {"type": "tool_reference", "tool_name": "get_weather"}, + {"type": "tool_reference", "tool_name": "search_docs"}, + ], + [ + {"type": "text", "text": "[Loaded tool: get_weather]"}, + {"type": "text", "text": "[Loaded tool: search_docs]"}, + ], + ), + ], +) +def test_translate_anthropic_messages_to_openai_tool_reference_results( + tool_references: list[dict[str, str]], expected_content: Any +): + anthropic_messages = [ + AnthopicMessagesAssistantMessageParam( + role="assistant", + content=[ + { + "type": "tool_use", + "id": "toolu_tool_search", + "name": "ToolSearch", + "input": {"query": "weather"}, + } + ], + ), + AnthropicMessagesUserMessageParam( + role="user", + content=[ + { + "type": "tool_result", + "tool_use_id": "toolu_tool_search", + "content": tool_references, + } + ], + ), + ] + + result = LiteLLMAnthropicMessagesAdapter().translate_anthropic_messages_to_openai( + messages=anthropic_messages + ) + + tool_messages = [ + message + for message in result + if isinstance(message, dict) and message.get("role") == "tool" + ] + assert tool_messages == [ + { + "role": "tool", + "tool_call_id": "toolu_tool_search", + "content": expected_content, + } + ] + + def test_streaming_chunk_with_both_text_and_tool_calls_issue_18238(): """ When a streaming choice contains both text content and tool_calls, diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_transformation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_transformation.py index 5ecf604f096..110146e2347 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_transformation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_transformation.py @@ -482,6 +482,33 @@ class TestTranslateMessagesToResponsesInput: result = _translate_messages(messages) assert result[0]["output"] == "Line 1\nLine 2" + def test_user_tool_result_list_content_preserves_tool_references(self): + messages = [ + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "call_search", + "content": [ + {"type": "text", "text": "Available:"}, + {"type": "tool_reference", "tool_name": "get_weather"}, + ], + } + ], + } + ] + + result = _translate_messages(messages) + + assert result == [ + { + "type": "function_call_output", + "call_id": "call_search", + "output": "Available:\n[Loaded tool: get_weather]", + } + ] + def test_user_tool_result_null_content(self): """tool_result with null content becomes empty string output.""" messages = [