From 66d67ae3563dbe31335aa06001478743afd80789 Mon Sep 17 00:00:00 2001 From: YutaSaito <36355491+uc4w6c@users.noreply.github.com> Date: Sat, 17 Jan 2026 06:01:12 +0900 Subject: [PATCH] Revert "Add sanititzation for anthropic messages" --- .../docs/completion/message_sanitization.md | 468 ------------------ docs/my-website/sidebars.js | 1 - .../prompt_templates/factory.py | 220 -------- .../anthropic/test_message_sanitization.py | 380 -------------- 4 files changed, 1069 deletions(-) delete mode 100644 docs/my-website/docs/completion/message_sanitization.md delete mode 100644 tests/test_litellm/llms/anthropic/test_message_sanitization.py diff --git a/docs/my-website/docs/completion/message_sanitization.md b/docs/my-website/docs/completion/message_sanitization.md deleted file mode 100644 index 0a1f766e2f..0000000000 --- a/docs/my-website/docs/completion/message_sanitization.md +++ /dev/null @@ -1,468 +0,0 @@ -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Message Sanitization for Tool Calling for anthropic models - -**Automatically fix common message formatting issues when using tool calling with `modify_params=True`** - -LiteLLM can automatically sanitize messages to handle common issues that occur during tool calling workflows, especially when using OpenAI-compatible clients with providers that have strict message format requirements (like Anthropic Claude). - -## Overview - -When `litellm.modify_params = True` is enabled, LiteLLM automatically sanitizes messages to fix three common issues: - -1. **Orphaned Tool Calls** - Assistant messages with tool_calls but missing tool results -2. **Orphaned Tool Results** - Tool messages that reference non-existent tool_call_ids -3. **Empty Message Content** - Messages with empty or whitespace-only text content - -This ensures your tool calling workflows work seamlessly across different LLM providers without manual message validation. - -## Why Message Sanitization? - -Different LLM providers have varying requirements for message formats, especially during tool calling: - -- **Anthropic Claude** requires every tool_call to have a corresponding tool result -- Some providers reject messages with empty content -- OpenAI-compatible clients may not always maintain perfect message consistency - -Without sanitization, these issues cause API errors that interrupt your workflows. With `modify_params=True`, LiteLLM handles these edge cases automatically. - -## Quick Start - - - - -```python -import litellm - -# Enable automatic message sanitization -litellm.modify_params = True - -# This will work even if messages have formatting issues -response = litellm.completion( - model="anthropic/claude-3-5-sonnet-20241022", - messages=[ - {"role": "user", "content": "What's the weather in Boston?"}, - { - "role": "assistant", - "tool_calls": [ - { - "id": "call_123", - "type": "function", - "function": {"name": "get_weather", "arguments": '{"city": "Boston"}'} - } - ] - # Missing tool result - LiteLLM will add a dummy result automatically - }, - {"role": "user", "content": "Thanks!"} - ], - tools=[{ - "type": "function", - "function": { - "name": "get_weather", - "description": "Get weather for a city", - "parameters": { - "type": "object", - "properties": {"city": {"type": "string"}}, - "required": ["city"] - } - } - }] -) -``` - - - - -```yaml -litellm_settings: - modify_params: true # Enable automatic message sanitization - -model_list: - - model_name: claude-3-5-sonnet - litellm_params: - model: anthropic/claude-3-5-sonnet-20241022 -``` - - - - -## Sanitization Cases - -### Case A: Orphaned Tool Calls (Missing Tool Results) - -**Problem:** An assistant message contains `tool_calls`, but no corresponding tool result messages follow. - -**Solution:** LiteLLM automatically adds dummy tool result messages for any missing tool results. - -**Example:** - -```python -import litellm -litellm.modify_params = True - -# Messages with orphaned tool calls -messages = [ - {"role": "user", "content": "Search for Python tutorials"}, - { - "role": "assistant", - "tool_calls": [ - { - "id": "call_abc123", - "type": "function", - "function": {"name": "web_search", "arguments": '{"query": "Python tutorials"}'} - } - ] - }, - # Missing tool result here! - {"role": "user", "content": "What about JavaScript?"} -] - -# LiteLLM automatically adds: -# { -# "role": "tool", -# "tool_call_id": "call_abc123", -# "content": "[System: Tool execution skipped/interrupted by user. No result provided for tool 'web_search'.]" -# } - -response = litellm.completion( - model="anthropic/claude-3-5-sonnet-20241022", - messages=messages, - tools=[...] -) -``` - -**When this happens:** -- User interrupts tool execution -- Client loses tool results due to network issues -- Conversation flow changes before tool completes -- Multi-turn conversations where tools are optional - -### Case B: Orphaned Tool Results (Invalid tool_call_id) - -**Problem:** A tool message references a `tool_call_id` that doesn't exist in any previous assistant message. - -**Solution:** LiteLLM automatically removes these orphaned tool result messages. - -**Example:** - -```python -import litellm -litellm.modify_params = True - -# Messages with orphaned tool result -messages = [ - {"role": "user", "content": "Hello"}, - {"role": "assistant", "content": "Hi! How can I help?"}, - { - "role": "tool", - "tool_call_id": "call_nonexistent", # This tool_call_id doesn't exist! - "content": "Some result" - } -] - -# LiteLLM automatically removes the orphaned tool message - -response = litellm.completion( - model="anthropic/claude-3-5-sonnet-20241022", - messages=messages -) -``` - -**When this happens:** -- Message history is manually edited -- Tool results are duplicated or mismatched -- Conversation state is restored incorrectly -- Messages are merged from different conversations - -### Case C: Empty Message Content - -**Problem:** User or assistant messages have empty or whitespace-only content. - -**Solution:** LiteLLM replaces empty content with a system placeholder message. - -**Example:** - -```python -import litellm -litellm.modify_params = True - -# Messages with empty content -messages = [ - {"role": "user", "content": ""}, # Empty content - {"role": "assistant", "content": " "}, # Whitespace only -] - -# LiteLLM automatically replaces with: -# {"role": "user", "content": "[System: Empty message content sanitised to satisfy protocol]"} -# {"role": "assistant", "content": "[System: Empty message content sanitised to satisfy protocol]"} - -response = litellm.completion( - model="anthropic/claude-3-5-sonnet-20241022", - messages=messages -) -``` - -**When this happens:** -- UI sends empty messages -- Content is stripped during preprocessing -- Placeholder messages in conversation history -- Edge cases in message construction - -## Configuration - -### Enable Globally - - - - -```python -import litellm - -# Enable for all completion calls -litellm.modify_params = True -``` - - - - -```yaml -litellm_settings: - modify_params: true -``` - - - - -```bash -export LITELLM_MODIFY_PARAMS=True -``` - - - - -### Enable Per-Request - -```python -import litellm - -# Enable only for specific requests -response = litellm.completion( - model="anthropic/claude-3-5-sonnet-20241022", - messages=messages, - modify_params=True # Override global setting -) -``` - -## Supported Providers - -Message sanitization works with all LLM providers that support tool calling: - -- ✅ Anthropic (Claude) -- ✅ OpenAI (GPT-4, GPT-3.5) -- ✅ AWS Bedrock (Claude, Titan) -- ✅ Google Vertex AI (Claude, Gemini) -- ✅ Azure OpenAI -- ✅ And all other providers with tool calling support - -## Implementation Details - -### How It Works - -The message sanitization process runs **before** messages are converted to provider-specific formats: - -1. **Input:** OpenAI-format messages with potential issues -2. **Sanitization:** Three helper functions process the messages: - - `_sanitize_empty_text_content()` - Fixes empty content - - `_add_missing_tool_results()` - Adds dummy tool results - - `_is_orphaned_tool_result()` - Identifies orphaned results -3. **Output:** Clean, provider-compatible messages - -### Code Reference - -The sanitization logic is implemented in: -- `litellm/litellm_core_utils/prompt_templates/factory.py` -- Function: `sanitize_messages_for_tool_calling()` - -### Logging - -When sanitization occurs, LiteLLM logs debug messages: - -```python -import litellm -litellm.set_verbose = True # Enable debug logging - -# You'll see logs like: -# "_add_missing_tool_results: Found 1 orphaned tool calls. Adding dummy tool results." -# "_is_orphaned_tool_result: Found orphaned tool result with tool_call_id=call_123" -# "_sanitize_empty_text_content: Replaced empty text content in user message" -``` - -## Best Practices - -### 1. Enable for Production Workflows - -```python -# Recommended for production -litellm.modify_params = True - -# Ensures robust handling of edge cases -response = litellm.completion( - model="anthropic/claude-3-5-sonnet-20241022", - messages=messages, - tools=tools -) -``` - -### 2. Preserve Tool Results When Possible - -While sanitization handles missing tool results, it's better to provide actual results: - -```python -# Good: Provide actual tool results -messages = [ - {"role": "user", "content": "Search for Python"}, - {"role": "assistant", "tool_calls": [...]}, - {"role": "tool", "tool_call_id": "call_123", "content": "Actual search results"} -] - -# Fallback: Sanitization adds dummy result if missing -messages = [ - {"role": "user", "content": "Search for Python"}, - {"role": "assistant", "tool_calls": [...]}, - # Missing tool result - sanitization adds dummy -] -``` - -### 3. Monitor Sanitization Events - -Use logging to track when sanitization occurs: - -```python -import litellm -import logging - -# Enable debug logging -litellm.set_verbose = True -logging.basicConfig(level=logging.DEBUG) - -# Track sanitization events in your application -response = litellm.completion( - model="anthropic/claude-3-5-sonnet-20241022", - messages=messages -) -``` - -### 4. Test Edge Cases - -Ensure your application handles sanitized messages correctly: - -```python -import litellm -litellm.modify_params = True - -# Test orphaned tool calls -test_messages = [ - {"role": "user", "content": "Test"}, - {"role": "assistant", "tool_calls": [{"id": "call_1", "type": "function", "function": {"name": "test", "arguments": "{}"}}]}, - {"role": "user", "content": "Continue"} # No tool result -] - -response = litellm.completion( - model="anthropic/claude-3-5-sonnet-20241022", - messages=test_messages, - tools=[...] -) - -# Verify the response handles the dummy tool result appropriately -``` - -## Related Features - -- **[Drop Params](./drop_params.md)** - Drop unsupported parameters for specific providers -- **[Message Trimming](./message_trimming.md)** - Trim messages to fit token limits -- **[Function Calling](./function_call.md)** - Complete guide to tool/function calling -- **[Reasoning Content](../reasoning_content.md)** - Extended thinking with tool calling - -## Troubleshooting - -### Sanitization Not Working - -**Issue:** Messages still cause errors despite `modify_params=True` - -**Solution:** -1. Verify `modify_params` is enabled: - ```python - import litellm - print(litellm.modify_params) # Should be True - ``` - -2. Check if the issue is provider-specific: - ```python - litellm.set_verbose = True # Enable debug logging - ``` - -3. Ensure you're using a recent version of LiteLLM: - ```bash - pip install --upgrade litellm - ``` - -### Unexpected Dummy Tool Results - -**Issue:** Dummy tool results appear when you expect actual results - -**Cause:** Tool result messages are missing or have incorrect `tool_call_id` - -**Solution:** -1. Verify tool result messages have correct `tool_call_id`: - ```python - # Correct - {"role": "tool", "tool_call_id": "call_123", "content": "result"} - - # Incorrect - will be treated as orphaned - {"role": "tool", "tool_call_id": "wrong_id", "content": "result"} - ``` - -2. Ensure tool results immediately follow assistant messages with tool_calls - -### Performance Impact - -**Issue:** Concerned about performance overhead - -**Details:** Message sanitization has minimal performance impact: -- Runs in O(n) time where n = number of messages -- Only processes messages when `modify_params=True` -- Typically adds < 1ms to request processing time - -## FAQ - -**Q: Does sanitization modify my original messages?** - -A: No, sanitization creates a new list of messages. Your original messages remain unchanged. - -**Q: Can I disable specific sanitization cases?** - -A: Currently, all three cases are handled together when `modify_params=True`. To disable sanitization entirely, set `modify_params=False`. - -**Q: What happens to the dummy tool results?** - -A: Dummy tool results are sent to the LLM provider along with other messages. The model sees them as regular tool results with informative error messages. - -**Q: Does this work with streaming?** - -A: Yes, message sanitization works with both streaming and non-streaming requests. - -**Q: Is this related to `drop_params`?** - -A: No, they're separate features: -- `modify_params` - Modifies/fixes message content and structure -- `drop_params` - Removes unsupported API parameters - -Both can be enabled simultaneously. - -## See Also - -- [Reasoning Content with Tool Calling](../reasoning_content.md) -- [Function Calling Guide](./function_call.md) -- [Bedrock Provider Documentation](../providers/bedrock.md) -- [Anthropic Provider Documentation](../providers/anthropic.md) diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index acc5d53855..38a26f6b18 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -822,7 +822,6 @@ const sidebars = { "completion/knowledgebase", "guides/code_interpreter", "completion/message_trimming", - "completion/message_sanitization", "completion/model_alias", "completion/mock_requests", "completion/predict_outputs", diff --git a/litellm/litellm_core_utils/prompt_templates/factory.py b/litellm/litellm_core_utils/prompt_templates/factory.py index 2311b34a2c..01bf18d79b 100644 --- a/litellm/litellm_core_utils/prompt_templates/factory.py +++ b/litellm/litellm_core_utils/prompt_templates/factory.py @@ -1989,223 +1989,6 @@ def anthropic_process_openai_file_message( ) -def _sanitize_empty_text_content( - message: AllMessageValues, -) -> AllMessageValues: - """ - Case C: Sanitize empty text content - - Replace empty or whitespace-only text content with a placeholder message. - - Returns: - The message with sanitized content if needed, otherwise the original message - """ - if message.get("role") in ["user", "assistant"]: - content = message.get("content") - if isinstance(content, str): - if not content or not content.strip(): - message = dict(message) # Make a copy - message["content"] = "[System: Empty message content sanitised to satisfy protocol]" - verbose_logger.debug( - f"_sanitize_empty_text_content: Replaced empty text content in {message.get('role')} message" - ) - return message - - -def _add_missing_tool_results( - current_message: AllMessageValues, - messages: List[AllMessageValues], - current_index: int, -) -> List[AllMessageValues]: - """ - Case A: Missing tool_result for tool_use (orphaned tool calls) - - If an assistant message has tool_calls but no corresponding tool result follows, - add a dummy tool result message indicating the user did not provide the result. - - Returns: - A list containing the assistant message followed by any dummy tool results needed - """ - result_messages: List[AllMessageValues] = [] - tool_calls = current_message.get("tool_calls") - - if not tool_calls or len(tool_calls) == 0: - return [current_message] - - # Collect all tool_call_ids from this assistant message - expected_tool_call_ids = set() - for tool_call in tool_calls: - tool_call_id = None - if isinstance(tool_call, dict): - tool_call_id = tool_call.get("id") - else: - tool_call_id = getattr(tool_call, "id", None) - if tool_call_id: - expected_tool_call_ids.add(tool_call_id) - - found_tool_call_ids = set() - j = current_index + 1 - - while j < len(messages): - next_msg = messages[j] - next_role = next_msg.get("role") - - if next_role == "assistant": - break - - if next_role in ["tool", "function"]: - tool_call_id = next_msg.get("tool_call_id") - if tool_call_id: - found_tool_call_ids.add(tool_call_id) - - j += 1 - - # Find missing tool results - missing_tool_call_ids = expected_tool_call_ids - found_tool_call_ids - - if missing_tool_call_ids: - verbose_logger.debug( - f"_add_missing_tool_results: Found {len(missing_tool_call_ids)} orphaned tool calls. Adding dummy tool results." - ) - - result_messages.append(current_message) - - for tool_call_id in missing_tool_call_ids: - tool_name = "unknown_tool" - for tool_call in tool_calls: - tc_id = None - if isinstance(tool_call, dict): - tc_id = tool_call.get("id") - else: - tc_id = getattr(tool_call, "id", None) - - if tc_id == tool_call_id: - if isinstance(tool_call, dict): - function = tool_call.get("function", {}) - if isinstance(function, dict): - tool_name = function.get("name", "unknown_tool") - else: - tool_name = getattr(function, "name", "unknown_tool") - else: - function = getattr(tool_call, "function", None) - if function: - tool_name = getattr(function, "name", "unknown_tool") - break - - dummy_tool_result: ChatCompletionToolMessage = { - "role": "tool", - "tool_call_id": tool_call_id, - "content": f"[System: Tool execution skipped/interrupted by user. No result provided for tool '{tool_name}'.]", - } - result_messages.append(dummy_tool_result) - - return result_messages - - return [current_message] - - -def _is_orphaned_tool_result( - current_message: AllMessageValues, - sanitized_messages: List[AllMessageValues], -) -> bool: - """ - Case B: Orphaned tool_result (unexpected result) - - Check if a tool message references a tool_call_id that doesn't exist in the previous - assistant message. - - Returns: - True if this is an orphaned tool result that should be removed, False otherwise - """ - if current_message.get("role") not in ["tool", "function"]: - return False - - tool_call_id = current_message.get("tool_call_id") - - if not tool_call_id: - return False - - # Look back to find the most recent assistant message with tool_calls - found_matching_tool_call = False - - for j in range(len(sanitized_messages) - 1, -1, -1): - prev_msg = sanitized_messages[j] - if prev_msg.get("role") == "assistant": - tool_calls = prev_msg.get("tool_calls") - if tool_calls: - for tool_call in tool_calls: - tc_id = None - if isinstance(tool_call, dict): - tc_id = tool_call.get("id") - else: - tc_id = getattr(tool_call, "id", None) - - if tc_id == tool_call_id: - found_matching_tool_call = True - break - - break - - if not found_matching_tool_call: - verbose_logger.debug( - "_is_orphaned_tool_result: Found orphaned tool result with redacted tool_call_id" - ) - return True - - return False - - -def sanitize_messages_for_tool_calling( - messages: List[AllMessageValues], -) -> List[AllMessageValues]: - """ - Sanitize messages for tool calling to handle common issues when modify_params=True: - - Case A: Missing tool_result for tool_use (orphaned tool calls) - - If an assistant message has tool_calls but no corresponding tool result follows, - add a dummy tool result message indicating the user did not provide the result. - - Case B: Orphaned tool_result (unexpected result) - - If a tool message references a tool_call_id that doesn't exist in the previous - assistant message, remove that tool message. - - Case C: Empty text content - - Replace empty or whitespace-only text content with a placeholder message. - - This function operates on OpenAI format messages before they are converted to - provider-specific formats. - """ - if not litellm.modify_params: - return messages - - sanitized_messages: List[AllMessageValues] = [] - i = 0 - - while i < len(messages): - current_message = messages[i] - - # Case C: Sanitize empty text content - current_message = _sanitize_empty_text_content(current_message) - - # Case A: Check if assistant message has tool_calls without following tool results - if current_message.get("role") == "assistant": - result_messages = _add_missing_tool_results(current_message, messages, i) - - # If dummy tool results were added, extend sanitized_messages and continue - if len(result_messages) > 1: - sanitized_messages.extend(result_messages) - i += 1 - continue - - # Case B: Check for orphaned tool results - if _is_orphaned_tool_result(current_message, sanitized_messages): - i += 1 - continue # Skip this orphaned tool result - - # Add the message to sanitized list - sanitized_messages.append(current_message) - i += 1 - - return sanitized_messages - - def anthropic_messages_pt( # noqa: PLR0915 messages: List[AllMessageValues], model: str, @@ -2225,9 +2008,6 @@ def anthropic_messages_pt( # noqa: PLR0915 5. System messages are a separate param to the Messages API 6. Ensure we only accept role, content. (message.name is not supported) """ - # Sanitize messages for tool calling issues when modify_params=True - messages = sanitize_messages_for_tool_calling(messages) - # add role=tool support to allow function call result/error submission user_message_types = {"user", "tool", "function"} # reformat messages to ensure user/assistant are alternating, if there's either 2 consecutive 'user' messages or 2 consecutive 'assistant' message, merge them. diff --git a/tests/test_litellm/llms/anthropic/test_message_sanitization.py b/tests/test_litellm/llms/anthropic/test_message_sanitization.py deleted file mode 100644 index 489ef527b4..0000000000 --- a/tests/test_litellm/llms/anthropic/test_message_sanitization.py +++ /dev/null @@ -1,380 +0,0 @@ -""" -Test message sanitization for Anthropic API when modify_params=True - -Tests three cases: -A. Missing tool_result for tool_use (orphaned tool calls) -B. Orphaned tool_result without matching tool_use -C. Empty text content -""" - -import pytest -import sys -import os - -# Add the parent directory to the path so we can import litellm -sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../.."))) - -import litellm -from litellm.litellm_core_utils.prompt_templates.factory import ( - sanitize_messages_for_tool_calling, - anthropic_messages_pt, -) - - -class TestMessageSanitization: - """Test message sanitization for tool calling scenarios""" - - def setup_method(self): - """Setup for each test""" - # Save original modify_params value - self.original_modify_params = litellm.modify_params - litellm.modify_params = True - - def teardown_method(self): - """Cleanup after each test""" - # Restore original modify_params value - litellm.modify_params = self.original_modify_params - - def test_case_a_orphaned_tool_call_single(self): - """ - Test Case A: Assistant message with tool_calls but no tool result - Should add a dummy tool result message - """ - messages = [ - { - "role": "user", - "content": "What is the weather in Nashik?" - }, - { - "role": "assistant", - "content": None, - "tool_calls": [ - { - "id": "toolu_01Kus2cC3ydjBW7UK4GJqBP4", - "type": "function", - "function": { - "name": "get_weather", - "arguments": '{"location": "Nashik, India"}' - } - } - ] - } - ] - - sanitized = sanitize_messages_for_tool_calling(messages) - - # Should have 3 messages: user, assistant, and dummy tool result - assert len(sanitized) == 3 - assert sanitized[0]["role"] == "user" - assert sanitized[1]["role"] == "assistant" - assert sanitized[2]["role"] == "tool" - assert sanitized[2]["tool_call_id"] == "toolu_01Kus2cC3ydjBW7UK4GJqBP4" - assert "skipped" in sanitized[2]["content"].lower() or "interrupted" in sanitized[2]["content"].lower() - assert "get_weather" in sanitized[2]["content"] - - def test_case_a_orphaned_tool_call_multiple(self): - """ - Test Case A: Assistant message with multiple tool_calls, some missing results - """ - messages = [ - { - "role": "user", - "content": "Get weather for Nashik and Mumbai" - }, - { - "role": "assistant", - "content": None, - "tool_calls": [ - { - "id": "call_1", - "type": "function", - "function": { - "name": "get_weather", - "arguments": '{"location": "Nashik"}' - } - }, - { - "id": "call_2", - "type": "function", - "function": { - "name": "get_weather", - "arguments": '{"location": "Mumbai"}' - } - } - ] - }, - { - "role": "tool", - "tool_call_id": "call_1", - "content": "Weather in Nashik: 25°C" - } - ] - - sanitized = sanitize_messages_for_tool_calling(messages) - - # Should have 4 messages: user, assistant, tool result for call_1, dummy for call_2 - assert len(sanitized) == 4 - assert sanitized[0]["role"] == "user" - assert sanitized[1]["role"] == "assistant" - assert sanitized[2]["tool_call_id"] == "call_2" # Dummy added first - assert sanitized[3]["tool_call_id"] == "call_1" # Original tool result - - def test_case_b_orphaned_tool_result(self): - """ - Test Case B: Tool result without matching tool_call in previous assistant message - Should remove the orphaned tool result - """ - messages = [ - { - "role": "user", - "content": "Hello" - }, - { - "role": "assistant", - "content": "Hi there!" - }, - { - "role": "tool", - "tool_call_id": "nonexistent_id", - "content": "Some result" - } - ] - - sanitized = sanitize_messages_for_tool_calling(messages) - - # Should have only 2 messages, orphaned tool result removed - assert len(sanitized) == 2 - assert sanitized[0]["role"] == "user" - assert sanitized[1]["role"] == "assistant" - - def test_case_b_valid_tool_result_preserved(self): - """ - Test Case B: Valid tool result with matching tool_call should be preserved - """ - messages = [ - { - "role": "user", - "content": "What's the weather?" - }, - { - "role": "assistant", - "content": None, - "tool_calls": [ - { - "id": "call_123", - "type": "function", - "function": { - "name": "get_weather", - "arguments": '{"location": "Boston"}' - } - } - ] - }, - { - "role": "tool", - "tool_call_id": "call_123", - "content": "Weather: 20°C" - } - ] - - sanitized = sanitize_messages_for_tool_calling(messages) - - # All messages should be preserved - assert len(sanitized) == 3 - assert sanitized[2]["role"] == "tool" - assert sanitized[2]["tool_call_id"] == "call_123" - - def test_case_c_empty_text_content_user(self): - """ - Test Case C: Empty text content in user message - Should replace with placeholder - """ - messages = [ - { - "role": "user", - "content": "" - }, - { - "role": "assistant", - "content": "Hello!" - } - ] - - sanitized = sanitize_messages_for_tool_calling(messages) - - assert len(sanitized) == 2 - assert sanitized[0]["role"] == "user" - assert sanitized[0]["content"] == "[System: Empty message content sanitised to satisfy protocol]" - - def test_case_c_whitespace_only_content(self): - """ - Test Case C: Whitespace-only content - Should replace with placeholder - """ - messages = [ - { - "role": "user", - "content": " \n \t " - }, - { - "role": "assistant", - "content": " " - } - ] - - sanitized = sanitize_messages_for_tool_calling(messages) - - assert len(sanitized) == 2 - assert sanitized[0]["content"] == "[System: Empty message content sanitised to satisfy protocol]" - assert sanitized[1]["content"] == "[System: Empty message content sanitised to satisfy protocol]" - - def test_case_c_valid_content_preserved(self): - """ - Test Case C: Valid non-empty content should be preserved - """ - messages = [ - { - "role": "user", - "content": "Hello" - }, - { - "role": "assistant", - "content": "Hi there!" - } - ] - - sanitized = sanitize_messages_for_tool_calling(messages) - - assert len(sanitized) == 2 - assert sanitized[0]["content"] == "Hello" - assert sanitized[1]["content"] == "Hi there!" - - def test_combined_cases(self): - """ - Test combination of multiple cases - """ - messages = [ - { - "role": "user", - "content": "Get weather" - }, - { - "role": "assistant", - "content": None, - "tool_calls": [ - { - "id": "call_1", - "type": "function", - "function": { - "name": "get_weather", - "arguments": '{"location": "NYC"}' - } - } - ] - }, - # Missing tool result for call_1 - { - "role": "user", - "content": "" # Empty content - }, - { - "role": "assistant", - "content": "Response" - }, - { - "role": "tool", - "tool_call_id": "orphaned_id", # Orphaned tool result - "content": "Some data" - } - ] - - sanitized = sanitize_messages_for_tool_calling(messages) - - # Should have: user, assistant, dummy tool result, user (sanitized), assistant - # Orphaned tool result should be removed - assert len(sanitized) == 5 - assert sanitized[0]["role"] == "user" - assert sanitized[1]["role"] == "assistant" - assert sanitized[2]["role"] == "tool" - assert sanitized[2]["tool_call_id"] == "call_1" # Dummy added - assert sanitized[3]["role"] == "user" - assert sanitized[3]["content"] == "[System: Empty message content sanitised to satisfy protocol]" - assert sanitized[4]["role"] == "assistant" - - def test_modify_params_false_no_sanitization(self): - """ - Test that sanitization is skipped when modify_params=False - """ - litellm.modify_params = False - - messages = [ - { - "role": "user", - "content": "" - }, - { - "role": "assistant", - "content": None, - "tool_calls": [ - { - "id": "call_1", - "type": "function", - "function": { - "name": "get_weather", - "arguments": '{}' - } - } - ] - } - ] - - sanitized = sanitize_messages_for_tool_calling(messages) - - # Messages should be unchanged - assert len(sanitized) == 2 - assert sanitized[0]["content"] == "" - assert len(sanitized[1].get("tool_calls", [])) == 1 - - def test_anthropic_messages_pt_integration(self): - """ - Test that sanitization is integrated into anthropic_messages_pt - """ - litellm.modify_params = True - - messages = [ - { - "role": "user", - "content": "What is the weather in Nashik?" - }, - { - "role": "assistant", - "content": None, - "tool_calls": [ - { - "id": "toolu_01Kus2cC3ydjBW7UK4GJqBP4", - "type": "function", - "function": { - "name": "get_weather", - "arguments": '{"location": "Nashik, India"}' - } - } - ] - } - ] - - # This should not raise an error and should add dummy tool result - result = anthropic_messages_pt( - messages=messages, - model="claude-sonnet-4-5", - llm_provider="anthropic" - ) - - # Should have at least 2 messages (user and assistant) - # The tool result will be merged into user content - assert len(result) >= 2 - assert result[0]["role"] == "user" - assert result[1]["role"] == "assistant" - - -if __name__ == "__main__": - pytest.main([__file__, "-v"])