Repository: openai/openai-agents-python

Issue:
Web search tool updates
Enable the following using the built-in tool objectL:
- type: `"web_search_preview"` -> `"web_search"`
- domain filtering: `filters.allowed_domains: str[]`
- sources: `include=["web_search_call.action.sources"]`

```python
from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-5-mini",
    reasoning={"effort": "low"},
    tools=[
        {
            "type": "web_search",
            "filters": {
                "allowed_domains": [
                    "pubmed.ncbi.nlm.nih.gov",
                    "clinicaltrials.gov",
                    "www.who.int",
                    "www.cdc.gov",
                    "www.fda.gov",
                ]
            },
        }
    ],
    tool_choice="auto",
    include=["web_search_call.action.sources"],
    input="Please perform a web search on how semaglutide is used in the treatment of diabetes.",
)

# print(response)
for item in response.output:
    if item.type == "web_search_call":
        print(item.action.query)
        for source in item.action.sources:
            print(source)
```

references:
- https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses#domain-filtering
- https://x.com/OpenAIDevs/status/1960425260576334274


Discussion:

Patch:
diff --git a/examples/tools/web_search_filters.py b/examples/tools/web_search_filters.py
new file mode 100644
index 000000000..22b3864ea
--- /dev/null
+++ b/examples/tools/web_search_filters.py
@@ -0,0 +1,60 @@
+import asyncio
+from datetime import datetime
+
+from openai.types.responses.tool import WebSearchToolFilters
+from openai.types.shared.reasoning import Reasoning
+
+from agents import Agent, ModelSettings, Runner, WebSearchTool, trace
+
+# import logging
+# logging.basicConfig(level=logging.DEBUG)
+
+
+async def main():
+    agent = Agent(
+        name="WebOAI website searcher",
+        model="gpt-5-nano",
+        instructions="You are a helpful agent that can search openai.com resources.",
+        tools=[
+            WebSearchTool(
+                # https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses#domain-filtering
+                filters=WebSearchToolFilters(
+                    allowed_domains=[
+                        "openai.com",
+                        "developer.openai.com",
+                        "platform.openai.com",
+                        "help.openai.com",
+                    ],
+                ),
+                search_context_size="medium",
+            )
+        ],
+        model_settings=ModelSettings(
+            reasoning=Reasoning(effort="low"),
+            verbosity="low",
+            # https://platform.openai.com/docs/guides/tools-web-search?api-mode=responses#sources
+            response_include=["web_search_call.action.sources"],
+        ),
+    )
+
+    with trace("Web search example"):
+        today = datetime.now().strftime("%Y-%m-%d")
+        query = f"Write a summary of the latest OpenAI Platform updates for developers in the last few weeks (today is {today})."
+        result = await Runner.run(agent, query)
+
+        print()
+        print("### Sources ###")
+        print()
+        for item in result.new_items:
+            if item.type == "tool_call_item":
+                if item.raw_item.type == "web_search_call":
+                    for source in item.raw_item.action.sources:  # type: ignore [union-attr]
+                        print(f"- {source.url}")
+        print()
+        print("### Final output ###")
+        print()
+        print(result.final_output)
+
+
+if __name__ == "__main__":
+    asyncio.run(main())
diff --git a/src/agents/model_settings.py b/src/agents/model_settings.py
index 47161dd18..6a3dbd04c 100644
--- a/src/agents/model_settings.py
+++ b/src/agents/model_settings.py
@@ -120,7 +120,10 @@ class ModelSettings:
     """Whether to include usage chunk.
     Only available for Chat Completions API."""
 
-    response_include: list[ResponseIncludable] | None = None
+    # TODO: revisit ResponseIncludable | str if ResponseIncludable covers more cases
+    # We've added str to support missing ones like
+    # "web_search_call.action.sources" etc.
+    response_include: list[ResponseIncludable | str] | None = None
     """Additional output data to include in the model response.
     [include parameter](https://platform.openai.com/docs/api-reference/responses/create#responses-create-include)"""
 
diff --git a/src/agents/models/openai_responses.py b/src/agents/models/openai_responses.py
index 4352c99c7..6405bd586 100644
--- a/src/agents/models/openai_responses.py
+++ b/src/agents/models/openai_responses.py
@@ -14,7 +14,6 @@
     ResponseStreamEvent,
     ResponseTextConfigParam,
     ToolParam,
-    WebSearchToolParam,
     response_create_params,
 )
 from openai.types.responses.response_prompt_param import ResponsePromptParam
@@ -336,6 +335,11 @@ def convert_tool_choice(
             return {
                 "type": "file_search",
             }
+        elif tool_choice == "web_search":
+            return {
+                # TODO: revist the type: ignore comment when ToolChoice is updated in the future
+                "type": "web_search",  # type: ignore [typeddict-item]
+            }
         elif tool_choice == "web_search_preview":
             return {
                 "type": "web_search_preview",
@@ -416,12 +420,13 @@ def _convert_tool(cls, tool: Tool) -> tuple[ToolParam, ResponseIncludable | None
             }
             includes: ResponseIncludable | None = None
         elif isinstance(tool, WebSearchTool):
-            ws: WebSearchToolParam = {
-                "type": "web_search_preview",
-                "user_location": tool.user_location,
+            # TODO: revist the type: ignore comment when ToolParam is updated in the future
+            converted_tool = {
+                "type": "web_search",
+                "filters": tool.filters.model_dump() if tool.filters is not None else None,  # type: ignore [typeddict-item]
+                "user_location": tool.user_location,  # type: ignore [typeddict-item]
                 "search_context_size": tool.search_context_size,
             }
-            converted_tool = ws
             includes = None
         elif isinstance(tool, FileSearchTool):
             converted_tool = {
diff --git a/src/agents/tool.py b/src/agents/tool.py
index 12f43ee76..4624fbb52 100644
--- a/src/agents/tool.py
+++ b/src/agents/tool.py
@@ -12,6 +12,7 @@
     ResponseComputerToolCall,
 )
 from openai.types.responses.response_output_item import LocalShellCall, McpApprovalRequest
+from openai.types.responses.tool import WebSearchToolFilters
 from openai.types.responses.tool_param import CodeInterpreter, ImageGeneration, Mcp
 from openai.types.responses.web_search_tool_param import UserLocation
 from pydantic import ValidationError
@@ -133,6 +134,9 @@ class WebSearchTool:
     user_location: UserLocation | None = None
     """Optional location for the search. Lets you customize results to be relevant to a location."""
 
+    filters: WebSearchToolFilters | None = None
+    """A filter to apply based on file attributes."""
+
     search_context_size: Literal["low", "medium", "high"] = "medium"
     """The amount of context to use for the search."""
 


Test patch:
diff --git a/tests/test_openai_responses_converter.py b/tests/test_openai_responses_converter.py
index 8e4866656..155239887 100644
--- a/tests/test_openai_responses_converter.py
+++ b/tests/test_openai_responses_converter.py
@@ -162,14 +162,14 @@ def drag(self, path: list[tuple[int, int]]) -> None:
     types = [ct["type"] for ct in converted.tools]
     assert "function" in types
     assert "file_search" in types
-    assert "web_search_preview" in types
+    assert "web_search" in types
     assert "computer_use_preview" in types
     # Verify file search tool contains max_num_results and vector_store_ids
     file_params = next(ct for ct in converted.tools if ct["type"] == "file_search")
     assert file_params.get("max_num_results") == file_tool.max_num_results
     assert file_params.get("vector_store_ids") == file_tool.vector_store_ids
     # Verify web search tool contains user_location and search_context_size
-    web_params = next(ct for ct in converted.tools if ct["type"] == "web_search_preview")
+    web_params = next(ct for ct in converted.tools if ct["type"] == "web_search")
     assert web_params.get("user_location") == web_tool.user_location
     assert web_params.get("search_context_size") == web_tool.search_context_size
     # Verify computer tool contains environment and computed dimensions