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
10 changes: 10 additions & 0 deletions src/agents/models/chatcmpl_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ def items_to_messages(
preserve_tool_output_all_content: bool = False,
base_url: str | None = None,
should_replay_reasoning_content: ShouldReplayReasoningContent | None = None,
strict_feature_validation: bool = False,
) -> list[ChatCompletionMessageParam]:
"""
Convert a sequence of 'Item' objects into a list of ChatCompletionMessageParam.
Expand All @@ -493,6 +494,8 @@ def items_to_messages(
should_replay_reasoning_content: Optional hook that decides whether a
reasoning item should be replayed into the next assistant message as
`reasoning_content`.
strict_feature_validation: Whether to raise a UserError for Responses-only
features that Chat Completions cannot faithfully represent.

Rules:
- EasyInputMessage or InputMessage (role=user) => ChatCompletionUserMessageParam
Expand Down Expand Up @@ -748,6 +751,13 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
for c in all_output_content
if c.get("type") == "text"
]
if not tool_result_content and all_output_content:
if strict_feature_validation:
raise UserError(
"Chat Completions tool outputs cannot contain only non-text "
"content unless preserve_tool_output_all_content=True"
)
tool_result_content = "[non-text tool output omitted]"
msg: ChatCompletionToolMessageParam = {
"role": "tool",
"tool_call_id": func_output["call_id"],
Expand Down
4 changes: 2 additions & 2 deletions src/agents/models/multi_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ def __init__(
responses API.
openai_strict_feature_validation: Whether OpenAI Chat Completions models should raise
a UserError when callers pass Responses-only features such as previous_response_id,
conversation_id, or prompt. Defaults to False, which preserves the previous
ignore-and-warn behavior.
conversation_id, prompt, or non-text-only tool outputs. Defaults to False, which
preserves the default compatibility behavior.
openai_websocket_base_url: The websocket base URL to use for the OpenAI provider.
If not provided, the provider will use `OPENAI_WEBSOCKET_BASE_URL` when set.
openai_prefix_mode: Controls how ``openai/...`` model strings are interpreted.
Expand Down
1 change: 1 addition & 0 deletions src/agents/models/openai_chatcompletions.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,7 @@ async def _fetch_response(
model=self.model,
base_url=str(self._client.base_url),
should_replay_reasoning_content=self.should_replay_reasoning_content,
strict_feature_validation=self._strict_feature_validation,
)

if system_instructions:
Expand Down
4 changes: 2 additions & 2 deletions src/agents/models/openai_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ def __init__(
API.
strict_feature_validation: Whether Chat Completions models should raise a UserError
when callers pass Responses-only features such as previous_response_id,
conversation_id, or prompt. Defaults to False, which preserves the previous
ignore-and-warn behavior.
conversation_id, prompt, or non-text-only tool outputs. Defaults to False, which
preserves the default compatibility behavior.
agent_registration: Optional agent registration configuration.
responses_websocket_options: Optional low-level websocket keepalive options for the
OpenAI Responses websocket transport.
Expand Down
92 changes: 92 additions & 0 deletions tests/models/test_openai_chatcompletions_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,98 @@ def test_items_to_messages_with_function_output_item():
assert tool_msg["content"] == func_output_item["output"]


def test_items_to_messages_with_non_text_only_function_output_uses_placeholder_by_default():
"""Default conversion should keep running without sending an empty tool message."""
func_output_item: FunctionCallOutput = {
"type": "function_call_output",
"call_id": "somecall",
"output": [
{
"type": "input_image",
"image_url": "https://example.com/image.png",
}
],
}

messages = Converter.items_to_messages([func_output_item])

assert len(messages) == 1
tool_msg = messages[0]
assert tool_msg["role"] == "tool"
assert tool_msg["tool_call_id"] == func_output_item["call_id"]
assert tool_msg["content"] == "[non-text tool output omitted]"


def test_items_to_messages_with_non_text_only_function_output_raises_in_strict_mode():
"""Strict validation should fail explicitly instead of silently losing the output."""
func_output_item: FunctionCallOutput = {
"type": "function_call_output",
"call_id": "somecall",
"output": [
{
"type": "input_image",
"image_url": "https://example.com/image.png",
}
],
}

with pytest.raises(UserError, match="cannot contain only non-text content"):
Converter.items_to_messages([func_output_item], strict_feature_validation=True)


def test_items_to_messages_with_mixed_function_output_keeps_text_by_default():
"""Default conversion should preserve text parts and omit unsupported non-text parts."""
func_output_item: FunctionCallOutput = {
"type": "function_call_output",
"call_id": "somecall",
"output": [
{"type": "input_text", "text": "visible text"},
{
"type": "input_image",
"image_url": "https://example.com/image.png",
},
],
}

messages = Converter.items_to_messages([func_output_item])

assert len(messages) == 1
tool_msg = messages[0]
assert tool_msg["role"] == "tool"
assert tool_msg["tool_call_id"] == func_output_item["call_id"]
assert tool_msg["content"] == [{"type": "text", "text": "visible text"}]


def test_items_to_messages_can_preserve_non_text_function_output() -> None:
"""Compatible providers can opt in to preserving non-text tool output."""
func_output_item: FunctionCallOutput = {
"type": "function_call_output",
"call_id": "somecall",
"output": [
{
"type": "input_image",
"image_url": "https://example.com/image.png",
}
],
}

messages = Converter.items_to_messages(
[func_output_item],
preserve_tool_output_all_content=True,
)

assert len(messages) == 1
tool_msg = messages[0]
assert tool_msg["role"] == "tool"
assert tool_msg["tool_call_id"] == func_output_item["call_id"]
assert tool_msg["content"] == [
{
"type": "image_url",
"image_url": {"url": "https://example.com/image.png", "detail": "auto"},
}
]


def test_extract_all_and_text_content_for_strings_and_lists():
"""
The converter provides helpers for extracting user-supplied message content
Expand Down
Loading