mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-11 19:48:29 +00:00
feat(types): add output_text property to ResponsesAPIResponse (#18491)
Add the output_text convenience property to ResponsesAPIResponse that aggregates all output_text items from the output list, matching the OpenAI SDK's Response.output_text behavior. The property iterates through output items, collects text content from message-type outputs, and returns them concatenated into a single string. Returns empty string if no output_text content exists. Handles both dict and Pydantic model access patterns for compatibility with different output formats. Fixes #18470 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: yurekami <yurekami@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1197,6 +1197,39 @@ class ResponsesAPIResponse(BaseLiteLLMOpenAIResponseObject):
|
||||
# Define private attributes using PrivateAttr
|
||||
_hidden_params: dict = PrivateAttr(default_factory=dict)
|
||||
|
||||
@property
|
||||
def output_text(self) -> str:
|
||||
"""
|
||||
Convenience property that aggregates all `output_text` items from the `output` list.
|
||||
|
||||
If no `output_text` content blocks exist, then an empty string is returned.
|
||||
|
||||
This matches the OpenAI SDK's Response.output_text behavior.
|
||||
"""
|
||||
texts: List[str] = []
|
||||
for output_item in self.output:
|
||||
# Handle both dict and object access patterns
|
||||
if isinstance(output_item, dict):
|
||||
item_type = output_item.get("type")
|
||||
content = output_item.get("content", [])
|
||||
else:
|
||||
item_type = getattr(output_item, "type", None)
|
||||
content = getattr(output_item, "content", [])
|
||||
|
||||
if item_type == "message":
|
||||
for content_item in content:
|
||||
if isinstance(content_item, dict):
|
||||
content_type = content_item.get("type")
|
||||
text = content_item.get("text", "")
|
||||
else:
|
||||
content_type = getattr(content_item, "type", None)
|
||||
text = getattr(content_item, "text", "") or ""
|
||||
|
||||
if content_type == "output_text":
|
||||
texts.append(text)
|
||||
|
||||
return "".join(texts)
|
||||
|
||||
|
||||
class ResponsesAPIStreamEvents(str, Enum):
|
||||
"""
|
||||
|
||||
@@ -35,3 +35,137 @@ def test_output_item_added_event():
|
||||
assert event.sequence_number == 4
|
||||
assert event.output_index == 1
|
||||
assert event.item is None
|
||||
|
||||
|
||||
class TestResponsesAPIResponseOutputText:
|
||||
"""Tests for the output_text property on ResponsesAPIResponse"""
|
||||
|
||||
def test_output_text_with_single_message(self):
|
||||
"""Test output_text with a single message containing text output"""
|
||||
from litellm.types.llms.openai import ResponsesAPIResponse
|
||||
|
||||
response = ResponsesAPIResponse(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
output=[
|
||||
{
|
||||
"type": "message",
|
||||
"id": "msg_123",
|
||||
"status": "completed",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": "Hello, world!",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert response.output_text == "Hello, world!"
|
||||
|
||||
def test_output_text_with_multiple_messages(self):
|
||||
"""Test output_text with multiple messages aggregates all text"""
|
||||
from litellm.types.llms.openai import ResponsesAPIResponse
|
||||
|
||||
response = ResponsesAPIResponse(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
output=[
|
||||
{
|
||||
"type": "message",
|
||||
"id": "msg_1",
|
||||
"status": "completed",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": "First part. ",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "message",
|
||||
"id": "msg_2",
|
||||
"status": "completed",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": "Second part.",
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
assert response.output_text == "First part. Second part."
|
||||
|
||||
def test_output_text_with_no_text_content(self):
|
||||
"""Test output_text returns empty string when no output_text content exists"""
|
||||
from litellm.types.llms.openai import ResponsesAPIResponse
|
||||
|
||||
response = ResponsesAPIResponse(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
output=[
|
||||
{
|
||||
"type": "function_call",
|
||||
"id": "call_123",
|
||||
"status": "completed",
|
||||
"name": "get_weather",
|
||||
"arguments": "{}",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert response.output_text == ""
|
||||
|
||||
def test_output_text_with_mixed_content(self):
|
||||
"""Test output_text only aggregates output_text type content"""
|
||||
from litellm.types.llms.openai import ResponsesAPIResponse
|
||||
|
||||
response = ResponsesAPIResponse(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
output=[
|
||||
{
|
||||
"type": "message",
|
||||
"id": "msg_1",
|
||||
"status": "completed",
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{
|
||||
"type": "output_text",
|
||||
"text": "The weather is sunny. ",
|
||||
},
|
||||
{
|
||||
"type": "refusal",
|
||||
"refusal": "I cannot do that.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "function_call",
|
||||
"id": "call_123",
|
||||
"status": "completed",
|
||||
"name": "get_weather",
|
||||
"arguments": "{}",
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
assert response.output_text == "The weather is sunny. "
|
||||
|
||||
def test_output_text_with_empty_output(self):
|
||||
"""Test output_text returns empty string with empty output list"""
|
||||
from litellm.types.llms.openai import ResponsesAPIResponse
|
||||
|
||||
response = ResponsesAPIResponse(
|
||||
id="resp_123",
|
||||
created_at=1234567890,
|
||||
output=[],
|
||||
)
|
||||
|
||||
assert response.output_text == ""
|
||||
|
||||
Reference in New Issue
Block a user