Skip to content

Commit 48ad4aa

Browse files
authored
fix: tolerate None text in ResponseOutputText content items (#2883)
1 parent 09ea6aa commit 48ad4aa

2 files changed

Lines changed: 29 additions & 1 deletion

File tree

src/agents/items.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,14 @@ def extract_text(cls, message: TResponseOutputItem) -> str | None:
679679
text = ""
680680
for content_item in message.content:
681681
if isinstance(content_item, ResponseOutputText):
682-
text += content_item.text
682+
# ``content_item.text`` is typed as ``str`` per the Responses
683+
# API schema, but provider gateways (e.g. LiteLLM) and
684+
# ``model_construct`` paths during streaming have been
685+
# observed surfacing ``None``. Coerce so callers — including
686+
# the SDK's own ``execute_tools_and_side_effects`` — don't
687+
# crash with ``TypeError: can only concatenate str (not
688+
# "NoneType") to str``.
689+
text += content_item.text or ""
683690

684691
return text or None
685692

tests/test_items_helpers.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,27 @@ def test_extract_text_concatenates_all_text_segments() -> None:
128128
)
129129

130130

131+
def test_extract_text_tolerates_none_text_content() -> None:
132+
"""Regression: ``content_item.text`` can be ``None`` when output items
133+
are assembled via ``model_construct`` (e.g. partial streaming responses)
134+
or surfaced through provider gateways like LiteLLM. Without the ``or ""``
135+
guard, ``extract_text`` raised
136+
``TypeError: can only concatenate str (not "NoneType") to str`` deep
137+
inside ``execute_tools_and_side_effects`` and aborted the agent turn.
138+
"""
139+
none_text = ResponseOutputText.model_construct(
140+
annotations=[], text=None, type="output_text", logprobs=[]
141+
)
142+
real_text = ResponseOutputText(annotations=[], text="hello", type="output_text", logprobs=[])
143+
144+
# Single None-text item: result is None (since concatenated text is "").
145+
assert ItemHelpers.extract_text(make_message([none_text])) is None
146+
147+
# Mixed content: real text is preserved, None is skipped.
148+
assert ItemHelpers.extract_text(make_message([real_text, none_text])) == "hello"
149+
assert ItemHelpers.extract_text(make_message([none_text, real_text])) == "hello"
150+
151+
131152
def test_input_to_new_input_list_from_string() -> None:
132153
result = ItemHelpers.input_to_new_input_list("hi")
133154
# Should wrap the string into a list with a single dict containing content and user role.

0 commit comments

Comments
 (0)