Fix: Responses content can't be none

This commit is contained in:
Sameer Kankute
2026-01-14 12:13:18 +05:30
parent 8d204f0a84
commit 1b34d17935
3 changed files with 307 additions and 2 deletions
@@ -6616,6 +6616,20 @@
"supports_tool_choice": true
},
"cerebras/zai-glm-4.6": {
"deprecation_date": "2026-01-20",
"input_cost_per_token": 2.25e-06,
"litellm_provider": "cerebras",
"max_input_tokens": 128000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 2.75e-06,
"source": "https://www.cerebras.ai/pricing",
"supports_function_calling": true,
"supports_reasoning": true,
"supports_tool_choice": true
},
"cerebras/zai-glm-4.7": {
"input_cost_per_token": 2.25e-06,
"litellm_provider": "cerebras",
"max_input_tokens": 128000,
+2 -2
View File
@@ -1216,11 +1216,11 @@ class ResponsesAPIResponse(BaseLiteLLMOpenAIResponseObject):
item_type = getattr(output_item, "type", None)
content = getattr(output_item, "content", [])
if item_type == "message":
if item_type == "message" and content:
for content_item in content:
if isinstance(content_item, dict):
content_type = content_item.get("type")
text = content_item.get("text", "")
text = content_item.get("text", "") or ""
else:
content_type = getattr(content_item, "type", None)
text = getattr(content_item, "text", "") or ""
@@ -0,0 +1,291 @@
"""
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"])