mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-04 07:06:26 +00:00
292 lines
9.8 KiB
Python
292 lines
9.8 KiB
Python
"""
|
|
Test for fixing null text values in output_text content blocks.
|
|
|
|
This test verifies that LiteLLM properly handles streaming responses where
|
|
text content is None, preventing TypeErrors in downstream OpenAI-compatible SDKs.
|
|
|
|
Related issue: When using LiteLLM as an OpenAI-compatible proxy for self-hosted
|
|
models, streamed responses can contain output_text content blocks where text is null.
|
|
These responses are forwarded unchanged to downstream SDKs which expect text to always
|
|
be a string (or omitted), causing TypeErrors.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from litellm.types.llms.openai import ResponsesAPIResponse
|
|
|
|
|
|
class TestNullTextHandling:
|
|
"""Test suite for handling None/null text values in responses."""
|
|
|
|
def test_output_text_with_none_text_dict_access(self):
|
|
"""
|
|
Test that output_text property handles None text values correctly when using dict access.
|
|
|
|
This simulates the scenario where a self-hosted model returns a response with
|
|
text: null in the content block.
|
|
"""
|
|
# Create a response with None text value (simulating gpt-oss-120b behavior)
|
|
response_data = {
|
|
"id": "resp_test123",
|
|
"object": "response",
|
|
"created_at": 1234567890,
|
|
"status": "completed",
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"id": "msg_test123",
|
|
"status": "completed",
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "output_text",
|
|
"text": None, # This is the problematic case
|
|
"annotations": []
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
|
|
response = ResponsesAPIResponse(**response_data)
|
|
|
|
# Should not raise TypeError and should return empty string
|
|
assert response.output_text == ""
|
|
|
|
def test_output_text_with_none_text_object_access(self):
|
|
"""
|
|
Test that output_text property handles None text values correctly.
|
|
|
|
This test verifies the object access path (getattr) in the output_text property.
|
|
"""
|
|
response_data = {
|
|
"id": "resp_test456",
|
|
"object": "response",
|
|
"created_at": 1234567890,
|
|
"status": "completed",
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"id": "msg_test456",
|
|
"status": "completed",
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "output_text",
|
|
"text": None, # This is the problematic case
|
|
"annotations": []
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
|
|
response = ResponsesAPIResponse(**response_data)
|
|
|
|
# Should not raise TypeError and should return empty string
|
|
assert response.output_text == ""
|
|
|
|
def test_output_text_with_mixed_none_and_valid_text(self):
|
|
"""
|
|
Test that output_text properly concatenates when some text values are None.
|
|
"""
|
|
response_data = {
|
|
"id": "resp_test789",
|
|
"object": "response",
|
|
"created_at": 1234567890,
|
|
"status": "completed",
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"id": "msg_test789",
|
|
"status": "completed",
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "output_text",
|
|
"text": "Hello ",
|
|
"annotations": []
|
|
},
|
|
{
|
|
"type": "output_text",
|
|
"text": None, # Should be treated as empty string
|
|
"annotations": []
|
|
},
|
|
{
|
|
"type": "output_text",
|
|
"text": "world!",
|
|
"annotations": []
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
|
|
response = ResponsesAPIResponse(**response_data)
|
|
|
|
# Should concatenate non-None values, treating None as empty string
|
|
assert response.output_text == "Hello world!"
|
|
|
|
def test_output_text_with_empty_string(self):
|
|
"""
|
|
Test that empty strings are handled correctly (baseline test).
|
|
"""
|
|
response_data = {
|
|
"id": "resp_test_empty",
|
|
"object": "response",
|
|
"created_at": 1234567890,
|
|
"status": "completed",
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"id": "msg_test_empty",
|
|
"status": "completed",
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "output_text",
|
|
"text": "",
|
|
"annotations": []
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
|
|
response = ResponsesAPIResponse(**response_data)
|
|
|
|
# Should return empty string
|
|
assert response.output_text == ""
|
|
|
|
def test_output_text_with_valid_text(self):
|
|
"""
|
|
Test that valid text values work correctly (baseline test).
|
|
"""
|
|
response_data = {
|
|
"id": "resp_test_valid",
|
|
"object": "response",
|
|
"created_at": 1234567890,
|
|
"status": "completed",
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"id": "msg_test_valid",
|
|
"status": "completed",
|
|
"role": "assistant",
|
|
"content": [
|
|
{
|
|
"type": "output_text",
|
|
"text": "This is a valid response",
|
|
"annotations": []
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
|
|
response = ResponsesAPIResponse(**response_data)
|
|
|
|
# Should return the text as-is
|
|
assert response.output_text == "This is a valid response"
|
|
|
|
def test_output_text_no_output_text_content(self):
|
|
"""
|
|
Test that responses without output_text content return empty string.
|
|
"""
|
|
response_data = {
|
|
"id": "resp_test_no_content",
|
|
"object": "response",
|
|
"created_at": 1234567890,
|
|
"status": "completed",
|
|
"output": [
|
|
{
|
|
"type": "message",
|
|
"id": "msg_test_no_content",
|
|
"status": "completed",
|
|
"role": "assistant",
|
|
"content": []
|
|
}
|
|
]
|
|
}
|
|
|
|
response = ResponsesAPIResponse(**response_data)
|
|
|
|
# Should return empty string when no output_text content exists
|
|
assert response.output_text == ""
|
|
|
|
|
|
class TestStreamingIteratorTextHandling:
|
|
"""Test suite for streaming iterator text handling."""
|
|
|
|
def test_content_part_added_event_has_empty_string_text(self):
|
|
"""
|
|
Test that ContentPartAddedEvent is created with empty string, not None.
|
|
"""
|
|
from unittest.mock import Mock
|
|
|
|
from litellm.responses.litellm_completion_transformation.streaming_iterator import (
|
|
LiteLLMCompletionStreamingIterator,
|
|
)
|
|
from litellm.types.llms.openai import (
|
|
ResponseInputParam,
|
|
ResponsesAPIOptionalRequestParams,
|
|
)
|
|
|
|
# Create a mock stream wrapper
|
|
mock_wrapper = Mock()
|
|
mock_wrapper.logging_obj = Mock()
|
|
|
|
iterator = LiteLLMCompletionStreamingIterator(
|
|
model="gpt-oss-120b",
|
|
litellm_custom_stream_wrapper=mock_wrapper,
|
|
request_input="test input",
|
|
responses_api_request={},
|
|
)
|
|
|
|
event = iterator.create_content_part_added_event()
|
|
|
|
# Verify that the part has text field set to empty string, not None
|
|
part_dict = event.part.model_dump() if hasattr(event.part, 'model_dump') else dict(event.part)
|
|
assert "text" in part_dict
|
|
assert part_dict["text"] == ""
|
|
assert part_dict["text"] is not None
|
|
|
|
def test_delta_string_from_none_content(self):
|
|
"""
|
|
Test that _get_delta_string_from_streaming_choices returns empty string for None content.
|
|
"""
|
|
from unittest.mock import Mock
|
|
|
|
from litellm.responses.litellm_completion_transformation.streaming_iterator import (
|
|
LiteLLMCompletionStreamingIterator,
|
|
)
|
|
from litellm.types.utils import Delta, StreamingChoices
|
|
|
|
# Create a mock stream wrapper
|
|
mock_wrapper = Mock()
|
|
mock_wrapper.logging_obj = Mock()
|
|
|
|
iterator = LiteLLMCompletionStreamingIterator(
|
|
model="gpt-oss-120b",
|
|
litellm_custom_stream_wrapper=mock_wrapper,
|
|
request_input="test input",
|
|
responses_api_request={},
|
|
)
|
|
|
|
# Create a choice with None content
|
|
choice = StreamingChoices(
|
|
index=0,
|
|
delta=Delta(content=None, role="assistant"),
|
|
finish_reason=None
|
|
)
|
|
|
|
result = iterator._get_delta_string_from_streaming_choices([choice])
|
|
|
|
# Should return empty string, not None
|
|
assert result == ""
|
|
assert result is not None
|
|
|
|
|
|
if __name__ == "__main__":
|
|
pytest.main([__file__, "-v"])
|