diff --git a/docs/tools.md b/docs/tools.md index d506b58c5f..7dde473160 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -37,7 +37,7 @@ OpenAI offers a few built-in tools when using the [`OpenAIResponsesModel`][agent Advanced hosted search options: - `FileSearchTool` supports `filters`, `ranking_options`, and `include_search_results` in addition to `vector_store_ids` and `max_num_results`. Set `max_num_results` to an integer from 1 through 50; `None` or zero uses the provider default. -- `WebSearchTool` supports `filters`, `user_location`, and `search_context_size`. +- `WebSearchTool` supports `filters`, `user_location`, `search_context_size`, `external_web_access`, and `search_content_types` with `image_settings` for image results. ```python from agents import Agent, FileSearchTool, Runner, WebSearchTool diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py index 87029d83e5..317c1c55bc 100644 --- a/src/agents/models/openai_responses.py +++ b/src/agents/models/openai_responses.py @@ -2183,9 +2183,18 @@ def _convert_tool( } if tool.external_web_access is not None: web_search_tool["external_web_access"] = tool.external_web_access + if tool.search_content_types is not None: + web_search_tool["search_content_types"] = list(tool.search_content_types) + if tool.image_settings is not None: + web_search_tool["image_settings"] = dict(tool.image_settings) + web_search_include: ResponseIncludable | None = ( + "web_search_call.results" + if tool.search_content_types is not None and "image" in tool.search_content_types + else None + ) return ( _require_responses_tool_param(web_search_tool), - None, + web_search_include, ) elif isinstance(tool, FileSearchTool): file_search_tool_param: FileSearchToolParam = { diff --git a/src/agents/tool.py b/src/agents/tool.py index 18a1399cca..629e8f5a7f 100644 --- a/src/agents/tool.py +++ b/src/agents/tool.py @@ -795,6 +795,16 @@ def name(self): return "file_search" +class WebSearchToolImageSettings(TypedDict, total=False): + """Image result settings for `WebSearchTool` when `search_content_types` includes `"image"`.""" + + max_results: int + """The number of image results to return.""" + + caption: bool + """Whether to include a short caption with each image when one is available.""" + + @dataclass class WebSearchTool: """A hosted tool that lets the LLM search the web. Currently only supported with OpenAI models, @@ -817,6 +827,16 @@ class WebSearchTool: indexed-only behavior where supported. """ + search_content_types: list[Literal["text", "image"]] | None = None + """The kinds of results the search may return. + + When omitted, the API default (text only) is used. Include `"image"` to + receive image results; pair it with `image_settings`. + """ + + image_settings: WebSearchToolImageSettings | None = None + """Settings for image results when `search_content_types` includes `"image"`.""" + if TYPE_CHECKING: def __init__( @@ -825,6 +845,8 @@ def __init__( filters: WebSearchToolFilters | dict[str, Any] | None = None, search_context_size: Literal["low", "medium", "high"] = "medium", external_web_access: bool | None = None, + search_content_types: list[Literal["text", "image"]] | None = None, + image_settings: WebSearchToolImageSettings | None = None, ) -> None: ... def __post_init__(self) -> None: diff --git a/tests/models/test_openai_responses_converter.py b/tests/models/test_openai_responses_converter.py index 42b5cb3671..068c98fed4 100644 --- a/tests/models/test_openai_responses_converter.py +++ b/tests/models/test_openai_responses_converter.py @@ -439,6 +439,8 @@ def test_convert_tools_basic_types_and_includes(): assert web_params.get("user_location") == web_tool.user_location assert web_params.get("search_context_size") == web_tool.search_context_size assert "external_web_access" not in web_params + assert "search_content_types" not in web_params + assert "image_settings" not in web_params # Verify computer tool uses the GA built-in tool payload. comp_params = next(ct for ct in converted.tools if ct["type"] == "computer") assert comp_params == {"type": "computer"} @@ -493,6 +495,37 @@ def test_convert_file_search_tool_rejects_unsupported_result_limits( Converter.convert_tools([tool], handoffs=[]) +def test_convert_tools_includes_web_search_content_types_and_image_settings() -> None: + web_tool = WebSearchTool( + search_content_types=["text", "image"], + image_settings={"max_results": 3, "caption": True}, + ) + + converted = Converter.convert_tools([web_tool], handoffs=[], model="gpt-5.6") + + # Image results arrive through the web_search_call.results include. + assert converted.includes == ["web_search_call.results"] + assert converted.tools == [ + { + "type": "web_search", + "filters": None, + "user_location": None, + "search_context_size": "medium", + "search_content_types": ["text", "image"], + "image_settings": {"max_results": 3, "caption": True}, + } + ] + + +def test_convert_tools_text_only_content_types_adds_no_include() -> None: + web_tool = WebSearchTool(search_content_types=["text"]) + + converted = Converter.convert_tools([web_tool], handoffs=[], model="gpt-5.6") + + assert converted.includes == [] + assert converted.tools[0].get("search_content_types") == ["text"] + + def test_convert_tools_includes_explicit_false_external_web_access() -> None: web_tool = WebSearchTool(external_web_access=False)