Python: Add search tool content for OpenAI responses (#5302)

* Add OpenAI search tool content parsing

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix typing

* simplified oai image test

* same for azure

* skip az responses api test

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Eduard van Valkenburg
2026-04-20 15:35:30 +02:00
committed by GitHub
Unverified
parent 60af59ba8b
commit 3e54a689fc
4 changed files with 417 additions and 37 deletions
@@ -549,6 +549,7 @@ class RawOpenAIChatClient( # type: ignore[misc]
chunk,
options=validated_options,
function_call_ids=function_call_ids,
seen_reasoning_delta_item_ids=seen_reasoning_delta_item_ids,
)
else:
async for chunk in await client.responses.create(stream=True, **run_options):
@@ -556,6 +557,7 @@ class RawOpenAIChatClient( # type: ignore[misc]
chunk,
options=validated_options,
function_call_ids=function_call_ids,
seen_reasoning_delta_item_ids=seen_reasoning_delta_item_ids,
)
except Exception as ex:
self._handle_request_error(ex)
@@ -1587,6 +1589,54 @@ class RawOpenAIChatClient( # type: ignore[misc]
"""Join shell commands into a single executable command string."""
return "\n".join(command for command in commands if command).strip()
@staticmethod
def _serialize_provider_payload(value: Any) -> Any:
"""Convert OpenAI SDK objects into JSON-serializable Python values."""
if isinstance(value, BaseModel):
return value.model_dump(mode="json", exclude_none=True)
if isinstance(value, Mapping):
return {str(key): RawOpenAIChatClient._serialize_provider_payload(item) for key, item in value.items()} # type: ignore[reportUnknownVariableType]
if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
return [RawOpenAIChatClient._serialize_provider_payload(item) for item in value] # type: ignore[reportUnknownVariableType]
return value
@staticmethod
def _get_search_tool_name(item_type: str) -> str:
"""Map OpenAI search output item types to unified content tool names."""
return "web_search" if item_type == "web_search_call" else "file_search"
def _parse_search_tool_call_content(self, item: Any) -> Content:
"""Create unified search tool call content from an OpenAI search output item."""
item_type = getattr(item, "type", "")
call_id = getattr(item, "id", None) or getattr(item, "call_id", None) or ""
if item_type == "web_search_call":
arguments = self._serialize_provider_payload(getattr(item, "action", None))
else:
arguments = {"queries": list(getattr(item, "queries", []) or [])}
return Content.from_search_tool_call(
call_id=call_id,
tool_name=self._get_search_tool_name(item_type),
arguments=arguments,
status=getattr(item, "status", None),
raw_representation=item,
)
def _parse_search_tool_result_content(self, item: Any) -> Content:
"""Create unified search tool result content from an OpenAI search output item."""
item_type = getattr(item, "type", "")
call_id = getattr(item, "id", None) or getattr(item, "call_id", None) or ""
if item_type == "web_search_call":
result = {"action": self._serialize_provider_payload(getattr(item, "action", None))}
else:
result = {"results": self._serialize_provider_payload(getattr(item, "results", None))}
return Content.from_search_tool_result(
call_id=call_id,
tool_name=self._get_search_tool_name(item_type),
result=result,
status=getattr(item, "status", None),
raw_representation=item,
)
# region Parse methods
def _parse_response_from_openai(
self,
@@ -1788,6 +1838,9 @@ class RawOpenAIChatClient( # type: ignore[misc]
raw_representation=item,
)
)
case "web_search_call" | "file_search_call":
contents.append(self._parse_search_tool_call_content(item))
contents.append(self._parse_search_tool_result_content(item))
case "mcp_approval_request": # ResponseOutputMcpApprovalRequest
contents.append(
Content.from_function_approval_request(
@@ -2377,8 +2430,19 @@ class RawOpenAIChatClient( # type: ignore[misc]
additional_properties=additional_properties_empty or None,
)
)
case "web_search_call" | "file_search_call":
contents.append(self._parse_search_tool_call_content(event_item))
case _:
logger.debug("Unparsed event of type: %s: %s", event.type, event)
case (
"response.web_search_call.in_progress"
| "response.web_search_call.searching"
| "response.web_search_call.completed"
| "response.file_search_call.in_progress"
| "response.file_search_call.searching"
| "response.file_search_call.completed"
):
pass
case "response.function_call_arguments.delta":
call_id, name = function_call_ids.get(event.output_index, (None, None))
if call_id and name:
@@ -2514,6 +2578,8 @@ class RawOpenAIChatClient( # type: ignore[misc]
raw_representation=done_item,
)
)
elif getattr(done_item, "type", None) in ("web_search_call", "file_search_call"):
contents.append(self._parse_search_tool_result_content(done_item))
case _:
logger.debug("Unparsed event of type: %s: %s", event.type, event)