Fix: tool content should be str

This commit is contained in:
Sameer Kankute 2026-01-07 11:01:31 +05:30
parent cc45423294
commit bf506378b8
2 changed files with 250 additions and 1 deletions

View file

@ -1,9 +1,11 @@
from typing import Optional, Tuple, Union
import json
from typing import List, Optional, Tuple, Union
import litellm
from litellm.constants import MIN_NON_ZERO_TEMPERATURE
from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig
from litellm.secret_managers.main import get_secret_str
from litellm.types.llms.openai import AllMessageValues
class DeepInfraConfig(OpenAIGPTConfig):
@ -117,6 +119,62 @@ class DeepInfraConfig(OpenAIGPTConfig):
optional_params[param] = value
return optional_params
def _transform_tool_message_content(self, messages: List[AllMessageValues]) -> List[AllMessageValues]:
"""
Transform tool message content from array to string format for DeepInfra compatibility.
DeepInfra requires tool message content to be a string, not an array.
This method converts tool message content from array format to string format.
Example transformation:
- Input: {"role": "tool", "content": [{"type": "text", "text": "20"}]}
- Output: {"role": "tool", "content": "20"}
Or if content is complex:
- Input: {"role": "tool", "content": [{"type": "text", "text": "result"}]}
- Output: {"role": "tool", "content": "[{\"type\": \"text\", \"text\": \"result\"}]"}
"""
for message in messages:
if message.get("role") == "tool":
content = message.get("content")
# If content is a list/array, convert it to string
if isinstance(content, list):
# Check if it's a simple single text item
if (
len(content) == 1
and isinstance(content[0], dict)
and content[0].get("type") == "text"
and "text" in content[0]
):
# Extract just the text value for simple cases
message["content"] = content[0]["text"]
else:
# For complex content, serialize the entire array as JSON string
message["content"] = json.dumps(content)
return messages
def _transform_messages(
self, messages: List[AllMessageValues], model: str, is_async: bool = False
):
"""
Transform messages for DeepInfra compatibility.
Handles both sync and async transformations.
"""
# First apply parent class transformations
parent_result = super()._transform_messages(messages=messages, model=model, is_async=is_async)
if is_async:
# If parent returns a coroutine, we need to await it and then apply our transformations
async def _async_transform():
transformed_messages = await parent_result
return self._transform_tool_message_content(transformed_messages)
return _async_transform()
else:
# For sync case, parent_result is already the transformed messages
return self._transform_tool_message_content(parent_result)
def _get_openai_compatible_provider_info(
self, api_base: Optional[str], api_key: Optional[str]
) -> Tuple[Optional[str], Optional[str]]:

View file

@ -24,3 +24,194 @@ def test_deepseek_supported_openai_params():
supported_openai_params = DeepInfraConfig().get_supported_openai_params(model="deepinfra/deepseek-ai/DeepSeek-V3.1")
print(supported_openai_params)
assert "reasoning_effort" in supported_openai_params
def test_deepinfra_tool_message_content_transformation():
"""
Test that DeepInfra transforms tool message content from array to string.
This fixes the issue where LibreChat sends tool messages with content as an array:
{"role": "tool", "content": [{"type": "text", "text": "20"}]}
DeepInfra requires content to be a string, so we transform it to:
{"role": "tool", "content": "20"}
Related to issue #13982
"""
from litellm.llms.deepinfra.chat.transformation import DeepInfraConfig
config = DeepInfraConfig()
# Test case 1: Simple single text item in array (common case from LibreChat)
messages_with_array_content = [
{
"role": "user",
"content": "Calculate 10 + 10"
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_123",
"type": "function",
"function": {
"name": "calculator",
"arguments": '{"input": "10 + 10"}'
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_123",
"name": "calculator",
"content": [{"type": "text", "text": "20"}] # Array format from LibreChat
}
]
transformed_messages = config._transform_messages(
messages=messages_with_array_content,
model="deepinfra/Qwen/Qwen3-235B-A22B"
)
# Verify the tool message content was converted to string
tool_message = transformed_messages[2]
assert tool_message["role"] == "tool"
assert isinstance(tool_message["content"], str)
assert tool_message["content"] == "20"
print(f"✓ Test case 1 passed: {tool_message['content']}")
# Test case 2: Complex content array (multiple items)
messages_with_complex_content = [
{
"role": "user",
"content": "Test"
},
{
"role": "assistant",
"tool_calls": [
{
"id": "call_456",
"type": "function",
"function": {"name": "test", "arguments": "{}"}
}
]
},
{
"role": "tool",
"tool_call_id": "call_456",
"content": [
{"type": "text", "text": "Result 1"},
{"type": "text", "text": "Result 2"}
]
}
]
transformed_messages_complex = config._transform_messages(
messages=messages_with_complex_content,
model="deepinfra/Qwen/Qwen3-235B-A22B"
)
tool_message_complex = transformed_messages_complex[2]
assert tool_message_complex["role"] == "tool"
assert isinstance(tool_message_complex["content"], str)
# For complex content, it should be JSON stringified
parsed_content = json.loads(tool_message_complex["content"])
assert len(parsed_content) == 2
assert parsed_content[0]["text"] == "Result 1"
print(f"✓ Test case 2 passed: {tool_message_complex['content']}")
# Test case 3: Tool message with string content (should remain unchanged)
messages_with_string_content = [
{
"role": "user",
"content": "Test"
},
{
"role": "assistant",
"tool_calls": [
{
"id": "call_789",
"type": "function",
"function": {"name": "test", "arguments": "{}"}
}
]
},
{
"role": "tool",
"tool_call_id": "call_789",
"content": "Simple string result" # Already a string
}
]
transformed_messages_string = config._transform_messages(
messages=messages_with_string_content,
model="deepinfra/Qwen/Qwen3-235B-A22B"
)
tool_message_string = transformed_messages_string[2]
assert tool_message_string["role"] == "tool"
assert isinstance(tool_message_string["content"], str)
assert tool_message_string["content"] == "Simple string result"
print(f"✓ Test case 3 passed: {tool_message_string['content']}")
print("\n✅ All DeepInfra tool message transformation tests passed!")
@pytest.mark.asyncio
async def test_deepinfra_tool_message_content_transformation_async():
"""
Test that DeepInfra transforms tool message content from array to string in async mode.
This ensures the async path works correctly when is_async=True.
Related to issue #13982
"""
from litellm.llms.deepinfra.chat.transformation import DeepInfraConfig
config = DeepInfraConfig()
# Test async transformation with tool message containing array content
messages_with_array_content = [
{
"role": "user",
"content": "Calculate 10 + 10"
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_123",
"type": "function",
"function": {
"name": "calculator",
"arguments": '{"input": "10 + 10"}'
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_123",
"name": "calculator",
"content": [{"type": "text", "text": "20"}] # Array format from LibreChat
}
]
# Call with is_async=True
transformed_messages = await config._transform_messages(
messages=messages_with_array_content,
model="deepinfra/Qwen/Qwen3-235B-A22B",
is_async=True
)
# Verify the tool message content was converted to string
tool_message = transformed_messages[2]
assert tool_message["role"] == "tool"
assert isinstance(tool_message["content"], str)
assert tool_message["content"] == "20"
print(f"✓ Async test passed: {tool_message['content']}")
print("\n✅ DeepInfra async tool message transformation test passed!")