@@ -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+
131152def 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