From 34733f7a988c09ec168989abcf0d81d136b02dc1 Mon Sep 17 00:00:00 2001 From: comp <323443+comp@users.noreply.github.com> Date: Tue, 7 Jul 2026 00:01:07 -0600 Subject: [PATCH] fix(items): coerce None refusal to empty string in extract_last_content --- src/agents/items.py | 7 ++++++- tests/test_items_helpers.py | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/agents/items.py b/src/agents/items.py index 6ecdeccd0b..bb64671af9 100644 --- a/src/agents/items.py +++ b/src/agents/items.py @@ -698,7 +698,12 @@ def extract_last_content(cls, message: TResponseOutputItem) -> str: # ``extract_text`` below. return last_content.text or "" elif isinstance(last_content, ResponseOutputRefusal): - return last_content.refusal + # ``last_content.refusal`` is typed as ``str`` per the Responses API schema, + # but provider gateways (e.g. LiteLLM) and ``model_construct`` paths during + # streaming have been observed surfacing ``None``. Coerce so callers relying + # on the ``-> str`` return type don't see a ``None``. Mirrors the text branch + # above and ``extract_refusal``. + return last_content.refusal or "" else: raise ModelBehaviorError(f"Unexpected content type: {type(last_content)}") diff --git a/tests/test_items_helpers.py b/tests/test_items_helpers.py index 567fae94d9..eb8f367d6a 100644 --- a/tests/test_items_helpers.py +++ b/tests/test_items_helpers.py @@ -84,6 +84,21 @@ def test_extract_last_content_of_refusal_message() -> None: assert ItemHelpers.extract_last_content(message) == "I cannot do that" +def test_extract_last_content_of_refusal_message_with_null_refusal() -> None: + # Provider gateways (e.g. LiteLLM) and ``model_construct`` streaming paths can surface a + # ``None`` refusal even though the schema types it as ``str``. Since ``extract_last_content`` + # is annotated ``-> str``, it must coerce ``None`` to "" rather than return ``None``. + refusal = ResponseOutputRefusal.model_construct(refusal=None, type="refusal") + message = ResponseOutputMessage.model_construct( + id="msg123", + content=[refusal], + role="assistant", + status="completed", + type="message", + ) + assert ItemHelpers.extract_last_content(message) == "" + + def test_extract_last_content_non_message_returns_empty() -> None: # Construct some other type of output item, e.g. a tool call, to verify non-message returns "". tool_call = ResponseFunctionToolCall(