From 0af4ec5494b353225459c93a61011f1a49a19ae8 Mon Sep 17 00:00:00 2001 From: ump45nose <52391318+ump45nose@users.noreply.github.com> Date: Wed, 12 Aug 2026 01:55:53 +0800 Subject: [PATCH 1/4] fix(anthropic): preserve tool reference results --- .../adapters/transformation.py | 15 +++++ litellm/types/llms/anthropic.py | 7 +- ...al_pass_through_adapters_transformation.py | 64 +++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) 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/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, From e77de1e81f0e7c4d1bd5e7734bd8686a20b2c9b6 Mon Sep 17 00:00:00 2001 From: ump45nose <52391318+ump45nose@users.noreply.github.com> Date: Wed, 12 Aug 2026 15:46:38 +0800 Subject: [PATCH 2/4] fix(anthropic): preserve tool references in responses --- .../responses_adapters/transformation.py | 6 ++++- .../test_responses_adapters_transformation.py | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) 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/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 = [ From b0735eef2e37c5d6baf2c1fb43c62d24ba827c94 Mon Sep 17 00:00:00 2001 From: yuwk <1729065730@qq.com> Date: Fri, 4 Sep 2026 23:15:00 +0800 Subject: [PATCH 3/4] fix(anthropic): use generator expression to stay within LIT002 budget --- .../responses_adapters/transformation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 863724dc7a6..9093724fc5a 100644 --- a/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py @@ -293,13 +293,13 @@ class LiteLLMAnthropicToResponsesAPIAdapter: elif isinstance(inner, str): output_text = inner elif isinstance(inner, list): - parts = [ + parts = ( 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( self._translate_anthropic_image_source_to_url(c.get("source")) From dfa26029207781fb1302e20c35a84375afa902af Mon Sep 17 00:00:00 2001 From: yuwk <1729065730@qq.com> Date: Sat, 5 Sep 2026 10:28:52 +0800 Subject: [PATCH 4/4] fix(anthropic): drop superseded adapters tool_reference test left over from main merge Upstream's _tool_result_part refactor now preserves tool_reference blocks as ChatCompletionToolReferenceObject on the /v1/messages path, so the older [Loaded tool: ...] text expectation in the adapters test is superseded. The responses_adapters path (LiteLLMAnthropicToResponsesAPIAdapter) still converts tool references to loaded-tool text and keeps its own regression test. --- ...al_pass_through_adapters_transformation.py | 64 ------------------- 1 file changed, 64 deletions(-) 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 431aa2d15bf..2d74c00071b 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 @@ -1645,70 +1645,6 @@ 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,