Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/agents/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,11 @@ 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) have been observed surfacing ``None``
# for typed-str fields (same rationale as the ``ResponseOutputText.text``
# guard above).
return last_content.refusal or ""
else:
raise ModelBehaviorError(f"Unexpected content type: {type(last_content)}")

Expand Down
28 changes: 27 additions & 1 deletion tests/utils/test_pretty_print_and_items.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from openai.types.responses import ResponseOutputMessage, ResponseOutputText
from openai.types.responses import ResponseOutputMessage, ResponseOutputRefusal, ResponseOutputText

from agents import Agent
from agents.exceptions import RunErrorDetails
Expand Down Expand Up @@ -62,6 +62,32 @@ def test_extract_last_content_returns_text_normally():
assert ItemHelpers.extract_last_content(msg) == "hello"


def _make_refusal_message(refusal: str | None) -> ResponseOutputMessage:
return ResponseOutputMessage.model_construct(
id="msg_refusal",
role="assistant",
status="completed",
content=[ResponseOutputRefusal.model_construct(type="refusal", refusal=refusal)],
)


def test_extract_last_content_returns_empty_string_for_none_refusal():
"""extract_last_content is declared ``-> str`` and must not return None even if
the underlying ResponseOutputRefusal.refusal is None. The same provider
gateways (e.g. LiteLLM) that surface ``None`` for ResponseOutputText.text
can also surface ``None`` for ResponseOutputRefusal.refusal; the ``or ""``
guard must be applied consistently to both branches."""
msg = _make_refusal_message(None)
result = ItemHelpers.extract_last_content(msg)
assert isinstance(result, str)
assert result == ""


def test_extract_last_content_returns_refusal_normally():
msg = _make_refusal_message("I cannot do that")
assert ItemHelpers.extract_last_content(msg) == "I cannot do that"


def _make_run_error_details(n_input: int = 0, n_output: int = 0) -> RunErrorDetails:
return RunErrorDetails(
input="hi",
Expand Down