From af42ce72095fb90aa2a5dfc9dd4e53b3c7931110 Mon Sep 17 00:00:00 2001 From: Gennaro Malafronte <8798987+malafronte@users.noreply.github.com> Date: Thu, 23 Apr 2026 00:29:01 +0200 Subject: [PATCH] test(anthropic): add think-tag regression coverage --- .../test_anthropic_chat_transformation.py | 35 ++++++++++++++ ...al_pass_through_adapters_transformation.py | 39 +++++++++++++++ ...erimental_pass_through_messages_handler.py | 48 +++++++++++++++++++ 3 files changed, 122 insertions(+) diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py index e1fe4befb57..867f6370961 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -511,6 +511,41 @@ def test_multiple_web_search_tool_results(): assert web_search_results[1]["tool_use_id"] == "srvtoolu_search2" +def test_extract_response_content_strips_leaked_think_tags_from_text_blocks(): + config = AnthropicConfig() + + completion_response = { + "content": [ + { + "type": "text", + "text": "I need to call the tool first.\n\n\ntool-loop-ok", + }, + { + "type": "tool_use", + "id": "toolu_01XYZ789", + "name": "echo_status", + "input": {"status": "ok"}, + }, + ] + } + + ( + text, + citations, + thinking_blocks, + reasoning_content, + tool_calls, + web_search_results, + tool_results, + compaction_blocks, + ) = config.extract_response_content(completion_response) + + assert text == "tool-loop-ok" + assert tool_calls is not None + assert len(tool_calls) == 1 + assert tool_calls[0]["function"]["name"] == "echo_status" + + def test_add_code_execution_tool(): config = AnthropicConfig() 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 42efde90926..8b7e5c3f05b 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 @@ -472,6 +472,45 @@ def test_translate_openai_response_to_anthropic_text_and_tool_calls(): assert anthropic_response.get("stop_reason") == "tool_use" +def test_translate_openai_response_to_anthropic_strips_leaked_think_tags(): + openai_response = ModelResponse( + id="resp_text_tool_sanitized", + model="gpt-4o-mini", + choices=[ + Choices( + finish_reason="tool_calls", + message=Message( + role="assistant", + content="I need to call the tool first.\n\n\ntool-loop-ok", + tool_calls=[ + ChatCompletionAssistantToolCall( + id="call_tool_combo", + type="function", + function=Function( + name="echo_status", arguments='{"status": "ok"}' + ), + ) + ], + ), + ) + ], + 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 anthropic_content[0]["type"] == "text" + assert anthropic_content[0]["text"] == "tool-loop-ok" + assert anthropic_content[1]["type"] == "tool_use" + assert anthropic_content[1]["name"] == "echo_status" + + def test_translate_streaming_openai_chunk_to_anthropic_with_partial_json(): """Test that partial tool arguments are correctly handled as input_json_delta.""" choices = [ diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py index 33628e1d19d..598513451e7 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py @@ -499,3 +499,51 @@ class TestThinkingSummaryPreservation: assert result == { "reasoning_effort": {"effort": "medium", "summary": "concise"} } + + +def test_anthropic_messages_handler_strips_leaked_think_tags_from_completion_path(): + from litellm.llms.anthropic.experimental_pass_through.messages.handler import ( + anthropic_messages_handler, + ) + from litellm.types.llms.anthropic_messages.anthropic_response import ( + AnthropicMessagesResponse, + ) + + leaked_response = AnthropicMessagesResponse( + id="msg_test", + type="message", + role="assistant", + content=[ + { + "type": "text", + "text": "I need to call the tool first.\n\n\ntool-loop-ok", + }, + { + "type": "tool_use", + "id": "toolu_01XYZ789", + "name": "echo_status", + "input": {"status": "ok"}, + }, + ], + model="custom-provider/test-model", + stop_reason="tool_use", + usage={"input_tokens": 10, "output_tokens": 20}, + ) + + with patch( + "litellm.llms.anthropic.experimental_pass_through.messages.handler.LiteLLMMessagesToCompletionTransformationHandler.anthropic_messages_handler", + return_value=leaked_response, + ) as mock_completion_handler: + result = anthropic_messages_handler( + max_tokens=100, + messages=[{"role": "user", "content": "Hello"}], + model="my-custom-model", + custom_llm_provider="my-custom-llm", + api_key="test-api-key", + ) + + mock_completion_handler.assert_called_once() + assert result["content"][0]["type"] == "text" + assert result["content"][0]["text"] == "tool-loop-ok" + assert result["content"][1]["type"] == "tool_use" + assert result["content"][1]["name"] == "echo_status"