mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
Fix: tool content should be str
This commit is contained in:
parent
cc45423294
commit
bf506378b8
2 changed files with 250 additions and 1 deletions
|
|
@ -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]]:
|
||||
|
|
|
|||
|
|
@ -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!")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue