mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: cleanup and refactoring of chat clients (#2937)
* refactoring and unifying naming schemes of internal methods of chat clients * set tool_choice to auto * fix for mypy * added note on naming and fix #2951 * fix responses * fixes in azure ai agents client
This commit is contained in:
committed by
GitHub
Unverified
parent
a71f768331
commit
e5c11d38d6
@@ -501,7 +501,7 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
stop: str | Sequence[str] | None = None,
|
||||
store: bool | None = None,
|
||||
temperature: float | None = None,
|
||||
tool_choice: ToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = None,
|
||||
tool_choice: ToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto",
|
||||
tools: ToolProtocol
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
@@ -535,6 +535,7 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
store: Whether to store the response.
|
||||
temperature: The sampling temperature to use.
|
||||
tool_choice: The tool choice for the request.
|
||||
Default is `auto`.
|
||||
tools: The tools to use for the request.
|
||||
top_p: The nucleus sampling probability to use.
|
||||
user: The user to associate with the request.
|
||||
@@ -595,7 +596,7 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
stop: str | Sequence[str] | None = None,
|
||||
store: bool | None = None,
|
||||
temperature: float | None = None,
|
||||
tool_choice: ToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = None,
|
||||
tool_choice: ToolMode | Literal["auto", "required", "none"] | dict[str, Any] | None = "auto",
|
||||
tools: ToolProtocol
|
||||
| Callable[..., Any]
|
||||
| MutableMapping[str, Any]
|
||||
@@ -629,6 +630,7 @@ class BaseChatClient(SerializationMixin, ABC):
|
||||
store: Whether to store the response.
|
||||
temperature: The sampling temperature to use.
|
||||
tool_choice: The tool choice for the request.
|
||||
Default is `auto`.
|
||||
tools: The tools to use for the request.
|
||||
top_p: The nucleus sampling probability to use.
|
||||
user: The user to associate with the request.
|
||||
|
||||
@@ -63,21 +63,21 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
def _mcp_prompt_message_to_chat_message(
|
||||
def _parse_message_from_mcp(
|
||||
mcp_type: types.PromptMessage | types.SamplingMessage,
|
||||
) -> ChatMessage:
|
||||
"""Convert a MCP container type to a Agent Framework type."""
|
||||
"""Parse an MCP container type into an Agent Framework type."""
|
||||
return ChatMessage(
|
||||
role=Role(value=mcp_type.role),
|
||||
contents=_mcp_type_to_ai_content(mcp_type.content),
|
||||
contents=_parse_content_from_mcp(mcp_type.content),
|
||||
raw_representation=mcp_type,
|
||||
)
|
||||
|
||||
|
||||
def _mcp_call_tool_result_to_ai_contents(
|
||||
def _parse_contents_from_mcp_tool_result(
|
||||
mcp_type: types.CallToolResult,
|
||||
) -> list[Contents]:
|
||||
"""Convert a MCP container type to a Agent Framework type.
|
||||
"""Parse an MCP CallToolResult into Agent Framework content types.
|
||||
|
||||
This function extracts the complete _meta field from CallToolResult objects
|
||||
and merges all metadata into the additional_properties field of converted
|
||||
@@ -111,7 +111,7 @@ def _mcp_call_tool_result_to_ai_contents(
|
||||
# Convert each content item and merge metadata
|
||||
result_contents = []
|
||||
for item in mcp_type.content:
|
||||
contents = _mcp_type_to_ai_content(item)
|
||||
contents = _parse_content_from_mcp(item)
|
||||
|
||||
if merged_meta_props:
|
||||
for content in contents:
|
||||
@@ -124,7 +124,7 @@ def _mcp_call_tool_result_to_ai_contents(
|
||||
return result_contents
|
||||
|
||||
|
||||
def _mcp_type_to_ai_content(
|
||||
def _parse_content_from_mcp(
|
||||
mcp_type: types.ImageContent
|
||||
| types.TextContent
|
||||
| types.AudioContent
|
||||
@@ -142,7 +142,7 @@ def _mcp_type_to_ai_content(
|
||||
| types.ToolResultContent
|
||||
],
|
||||
) -> list[Contents]:
|
||||
"""Convert a MCP type to a Agent Framework type."""
|
||||
"""Parse an MCP type into an Agent Framework type."""
|
||||
mcp_types = mcp_type if isinstance(mcp_type, Sequence) else [mcp_type]
|
||||
return_types: list[Contents] = []
|
||||
for mcp_type in mcp_types:
|
||||
@@ -178,7 +178,7 @@ def _mcp_type_to_ai_content(
|
||||
return_types.append(
|
||||
FunctionResultContent(
|
||||
call_id=mcp_type.toolUseId,
|
||||
result=_mcp_type_to_ai_content(mcp_type.content)
|
||||
result=_parse_content_from_mcp(mcp_type.content)
|
||||
if mcp_type.content
|
||||
else mcp_type.structuredContent,
|
||||
exception=Exception() if mcp_type.isError else None,
|
||||
@@ -211,10 +211,10 @@ def _mcp_type_to_ai_content(
|
||||
return return_types
|
||||
|
||||
|
||||
def _ai_content_to_mcp_types(
|
||||
def _prepare_content_for_mcp(
|
||||
content: Contents,
|
||||
) -> types.TextContent | types.ImageContent | types.AudioContent | types.EmbeddedResource | types.ResourceLink | None:
|
||||
"""Convert a BaseContent type to a MCP type."""
|
||||
"""Prepare an Agent Framework content type for MCP."""
|
||||
match content:
|
||||
case TextContent():
|
||||
return types.TextContent(type="text", text=content.text)
|
||||
@@ -253,15 +253,15 @@ def _ai_content_to_mcp_types(
|
||||
return None
|
||||
|
||||
|
||||
def _chat_message_to_mcp_types(
|
||||
def _prepare_message_for_mcp(
|
||||
content: ChatMessage,
|
||||
) -> list[types.TextContent | types.ImageContent | types.AudioContent | types.EmbeddedResource | types.ResourceLink]:
|
||||
"""Convert a ChatMessage to a list of MCP types."""
|
||||
"""Prepare a ChatMessage for MCP format."""
|
||||
messages: list[
|
||||
types.TextContent | types.ImageContent | types.AudioContent | types.EmbeddedResource | types.ResourceLink
|
||||
] = []
|
||||
for item in content.contents:
|
||||
mcp_content = _ai_content_to_mcp_types(item)
|
||||
mcp_content = _prepare_content_for_mcp(item)
|
||||
if mcp_content:
|
||||
messages.append(mcp_content)
|
||||
return messages
|
||||
@@ -469,7 +469,7 @@ class MCPTool:
|
||||
logger.debug("Sampling callback called with params: %s", params)
|
||||
messages: list[ChatMessage] = []
|
||||
for msg in params.messages:
|
||||
messages.append(_mcp_prompt_message_to_chat_message(msg))
|
||||
messages.append(_parse_message_from_mcp(msg))
|
||||
try:
|
||||
response = await self.chat_client.get_response(
|
||||
messages,
|
||||
@@ -487,7 +487,7 @@ class MCPTool:
|
||||
code=types.INTERNAL_ERROR,
|
||||
message="Failed to get chat message content.",
|
||||
)
|
||||
mcp_contents = _chat_message_to_mcp_types(response.messages[0])
|
||||
mcp_contents = _prepare_message_for_mcp(response.messages[0])
|
||||
# grab the first content that is of type TextContent or ImageContent
|
||||
mcp_content = next(
|
||||
(content for content in mcp_contents if isinstance(content, (types.TextContent, types.ImageContent))),
|
||||
@@ -692,7 +692,7 @@ class MCPTool:
|
||||
k: v for k, v in kwargs.items() if k not in {"chat_options", "tools", "tool_choice", "thread"}
|
||||
}
|
||||
try:
|
||||
return _mcp_call_tool_result_to_ai_contents(
|
||||
return _parse_contents_from_mcp_tool_result(
|
||||
await self.session.call_tool(tool_name, arguments=filtered_kwargs)
|
||||
)
|
||||
except McpError as mcp_exc:
|
||||
@@ -724,7 +724,7 @@ class MCPTool:
|
||||
)
|
||||
try:
|
||||
prompt_result = await self.session.get_prompt(prompt_name, arguments=kwargs)
|
||||
return [_mcp_prompt_message_to_chat_message(message) for message in prompt_result.messages]
|
||||
return [_parse_message_from_mcp(message) for message in prompt_result.messages]
|
||||
except McpError as mcp_exc:
|
||||
raise ToolExecutionException(mcp_exc.error.message, inner_exception=mcp_exc) from mcp_exc
|
||||
except Exception as ex:
|
||||
|
||||
@@ -1779,11 +1779,6 @@ def _handle_function_calls_response(
|
||||
response: "ChatResponse | None" = None
|
||||
fcc_messages: "list[ChatMessage]" = []
|
||||
|
||||
# If tools are provided but tool_choice is not set, default to "auto" for function invocation
|
||||
tools = _extract_tools(kwargs)
|
||||
if tools and kwargs.get("tool_choice") is None:
|
||||
kwargs["tool_choice"] = "auto"
|
||||
|
||||
for attempt_idx in range(config.max_iterations if config.enabled else 0):
|
||||
fcc_todo = _collect_approval_responses(prepped_messages)
|
||||
if fcc_todo:
|
||||
|
||||
@@ -154,7 +154,7 @@ class AzureOpenAIChatClient(AzureOpenAIConfigMixin, OpenAIBaseChatClient):
|
||||
)
|
||||
|
||||
@override
|
||||
def _parse_text_from_choice(self, choice: Choice | ChunkChoice) -> TextContent | None:
|
||||
def _parse_text_from_openai(self, choice: Choice | ChunkChoice) -> TextContent | None:
|
||||
"""Parse the choice into a TextContent object.
|
||||
|
||||
Overwritten from OpenAIBaseChatClient to deal with Azure On Your Data function.
|
||||
|
||||
@@ -164,7 +164,7 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
async def close(self) -> None:
|
||||
"""Clean up any assistants we created."""
|
||||
if self._should_delete_assistant and self.assistant_id is not None:
|
||||
client = await self.ensure_client()
|
||||
client = await self._ensure_client()
|
||||
await client.beta.assistants.delete(self.assistant_id)
|
||||
object.__setattr__(self, "assistant_id", None)
|
||||
object.__setattr__(self, "_should_delete_assistant", False)
|
||||
@@ -188,7 +188,7 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
# Extract necessary state from messages and options
|
||||
# prepare
|
||||
run_options, tool_results = self._prepare_options(messages, chat_options, **kwargs)
|
||||
|
||||
# Get the thread ID
|
||||
@@ -204,10 +204,10 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
# Determine which assistant to use and create if needed
|
||||
assistant_id = await self._get_assistant_id_or_create()
|
||||
|
||||
# Create the streaming response
|
||||
# execute
|
||||
stream, thread_id = await self._create_assistant_stream(thread_id, assistant_id, run_options, tool_results)
|
||||
|
||||
# Process and yield each update from the stream
|
||||
# process
|
||||
async for update in self._process_stream_events(stream, thread_id):
|
||||
yield update
|
||||
|
||||
@@ -222,7 +222,7 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
if not self.model_id:
|
||||
raise ServiceInitializationError("Parameter 'model_id' is required for assistant creation.")
|
||||
|
||||
client = await self.ensure_client()
|
||||
client = await self._ensure_client()
|
||||
created_assistant = await client.beta.assistants.create(
|
||||
model=self.model_id,
|
||||
description=self.assistant_description,
|
||||
@@ -245,11 +245,11 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
Returns:
|
||||
tuple: (stream, final_thread_id)
|
||||
"""
|
||||
client = await self.ensure_client()
|
||||
client = await self._ensure_client()
|
||||
# Get any active run for this thread
|
||||
thread_run = await self._get_active_thread_run(thread_id)
|
||||
|
||||
tool_run_id, tool_outputs = self._convert_function_results_to_tool_output(tool_results)
|
||||
tool_run_id, tool_outputs = self._prepare_tool_outputs_for_assistants(tool_results)
|
||||
|
||||
if thread_run is not None and tool_run_id is not None and tool_run_id == thread_run.id and tool_outputs:
|
||||
# There's an active run and we have tool results to submit, so submit the results.
|
||||
@@ -270,7 +270,7 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
|
||||
async def _get_active_thread_run(self, thread_id: str | None) -> Run | None:
|
||||
"""Get any active run for the given thread."""
|
||||
client = await self.ensure_client()
|
||||
client = await self._ensure_client()
|
||||
if thread_id is None:
|
||||
return None
|
||||
|
||||
@@ -281,7 +281,7 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
|
||||
async def _prepare_thread(self, thread_id: str | None, thread_run: Run | None, run_options: dict[str, Any]) -> str:
|
||||
"""Prepare the thread for a new run, creating or cleaning up as needed."""
|
||||
client = await self.ensure_client()
|
||||
client = await self._ensure_client()
|
||||
if thread_id is None:
|
||||
# No thread ID was provided, so create a new thread.
|
||||
thread = await client.beta.threads.create( # type: ignore[reportDeprecated]
|
||||
@@ -330,7 +330,7 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
response_id=response_id,
|
||||
)
|
||||
elif response.event == "thread.run.requires_action" and isinstance(response.data, Run):
|
||||
contents = self._create_function_call_contents(response.data, response_id)
|
||||
contents = self._parse_function_calls_from_assistants(response.data, response_id)
|
||||
if contents:
|
||||
yield ChatResponseUpdate(
|
||||
role=Role.ASSISTANT,
|
||||
@@ -371,8 +371,8 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
role=Role.ASSISTANT,
|
||||
)
|
||||
|
||||
def _create_function_call_contents(self, event_data: Run, response_id: str | None) -> list[Contents]:
|
||||
"""Create function call contents from a tool action event."""
|
||||
def _parse_function_calls_from_assistants(self, event_data: Run, response_id: str | None) -> list[Contents]:
|
||||
"""Parse function call contents from an assistants tool action event."""
|
||||
contents: list[Contents] = []
|
||||
|
||||
if event_data.required_action is not None:
|
||||
@@ -490,10 +490,11 @@ class OpenAIAssistantsClient(OpenAIConfigMixin, BaseChatClient):
|
||||
|
||||
return run_options, tool_results
|
||||
|
||||
def _convert_function_results_to_tool_output(
|
||||
def _prepare_tool_outputs_for_assistants(
|
||||
self,
|
||||
tool_results: list[FunctionResultContent] | None,
|
||||
) -> tuple[str | None, list[ToolOutput] | None]:
|
||||
"""Prepare function results for submission to the assistants API."""
|
||||
run_id: str | None = None
|
||||
tool_outputs: list[ToolOutput] | None = None
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from openai.types.chat.chat_completion import ChatCompletion, Choice
|
||||
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
|
||||
from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice
|
||||
from openai.types.chat.chat_completion_message_custom_tool_call import ChatCompletionMessageCustomToolCall
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from pydantic import ValidationError
|
||||
|
||||
from .._clients import BaseChatClient
|
||||
from .._logging import get_logger
|
||||
@@ -69,10 +69,12 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
client = await self.ensure_client()
|
||||
client = await self._ensure_client()
|
||||
# prepare
|
||||
options_dict = self._prepare_options(messages, chat_options)
|
||||
try:
|
||||
return self._create_chat_response(
|
||||
# execute and process
|
||||
return self._parse_response_from_openai(
|
||||
await client.chat.completions.create(stream=False, **options_dict), chat_options
|
||||
)
|
||||
except BadRequestError as ex:
|
||||
@@ -98,14 +100,16 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
client = await self.ensure_client()
|
||||
client = await self._ensure_client()
|
||||
# prepare
|
||||
options_dict = self._prepare_options(messages, chat_options)
|
||||
options_dict["stream_options"] = {"include_usage": True}
|
||||
try:
|
||||
# execute and process
|
||||
async for chunk in await client.chat.completions.create(stream=True, **options_dict):
|
||||
if len(chunk.choices) == 0 and chunk.usage is None:
|
||||
continue
|
||||
yield self._create_chat_response_update(chunk)
|
||||
yield self._parse_response_update_from_openai(chunk)
|
||||
except BadRequestError as ex:
|
||||
if ex.code == "content_filter":
|
||||
raise OpenAIContentFilterException(
|
||||
@@ -124,7 +128,9 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
|
||||
# region content creation
|
||||
|
||||
def _chat_to_tool_spec(self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]]) -> list[dict[str, Any]]:
|
||||
def _prepare_tools_for_openai(
|
||||
self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]]
|
||||
) -> list[dict[str, Any]]:
|
||||
chat_tools: list[dict[str, Any]] = []
|
||||
for tool in tools:
|
||||
if isinstance(tool, ToolProtocol):
|
||||
@@ -157,51 +163,65 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
return None
|
||||
|
||||
def _prepare_options(self, messages: MutableSequence[ChatMessage], chat_options: ChatOptions) -> dict[str, Any]:
|
||||
# Preprocess web search tool if it exists
|
||||
options_dict = chat_options.to_dict(
|
||||
run_options = chat_options.to_dict(
|
||||
exclude={
|
||||
"type",
|
||||
"instructions", # included as system message
|
||||
"allow_multiple_tool_calls", # handled separately
|
||||
"response_format", # handled separately
|
||||
"additional_properties", # handled separately
|
||||
}
|
||||
)
|
||||
|
||||
if messages and "messages" not in options_dict:
|
||||
options_dict["messages"] = self._prepare_chat_history_for_request(messages)
|
||||
if "messages" not in options_dict:
|
||||
# messages
|
||||
if messages and "messages" not in run_options:
|
||||
run_options["messages"] = self._prepare_messages_for_openai(messages)
|
||||
if "messages" not in run_options:
|
||||
raise ServiceInvalidRequestError("Messages are required for chat completions")
|
||||
|
||||
# Translation between ChatOptions and Chat Completion API
|
||||
translations = {
|
||||
"model_id": "model",
|
||||
"allow_multiple_tool_calls": "parallel_tool_calls",
|
||||
"max_tokens": "max_output_tokens",
|
||||
}
|
||||
for old_key, new_key in translations.items():
|
||||
if old_key in run_options and old_key != new_key:
|
||||
run_options[new_key] = run_options.pop(old_key)
|
||||
|
||||
# model id
|
||||
if not run_options.get("model"):
|
||||
if not self.model_id:
|
||||
raise ValueError("model_id must be a non-empty string")
|
||||
run_options["model"] = self.model_id
|
||||
|
||||
# tools
|
||||
if chat_options.tools is not None:
|
||||
web_search_options = self._process_web_search_tool(chat_options.tools)
|
||||
if web_search_options:
|
||||
options_dict["web_search_options"] = web_search_options
|
||||
options_dict["tools"] = self._chat_to_tool_spec(chat_options.tools)
|
||||
if chat_options.allow_multiple_tool_calls is not None:
|
||||
options_dict["parallel_tool_calls"] = chat_options.allow_multiple_tool_calls
|
||||
if not options_dict.get("tools", None):
|
||||
options_dict.pop("tools", None)
|
||||
options_dict.pop("parallel_tool_calls", None)
|
||||
options_dict.pop("tool_choice", None)
|
||||
# Preprocess web search tool if it exists
|
||||
if web_search_options := self._process_web_search_tool(chat_options.tools):
|
||||
run_options["web_search_options"] = web_search_options
|
||||
run_options["tools"] = self._prepare_tools_for_openai(chat_options.tools)
|
||||
if not run_options.get("tools", None):
|
||||
run_options.pop("tools", None)
|
||||
run_options.pop("parallel_tool_calls", None)
|
||||
run_options.pop("tool_choice", None)
|
||||
# tool choice when `tool_choice` is a dict with single key `mode`, extract the mode value
|
||||
if (tool_choice := run_options.get("tool_choice")) and len(tool_choice.keys()) == 1:
|
||||
run_options["tool_choice"] = tool_choice["mode"]
|
||||
|
||||
if "model_id" not in options_dict:
|
||||
options_dict["model"] = self.model_id
|
||||
else:
|
||||
options_dict["model"] = options_dict.pop("model_id")
|
||||
if (
|
||||
chat_options.response_format
|
||||
and isinstance(chat_options.response_format, type)
|
||||
and issubclass(chat_options.response_format, BaseModel)
|
||||
):
|
||||
options_dict["response_format"] = type_to_response_format_param(chat_options.response_format)
|
||||
if additional_properties := options_dict.pop("additional_properties", None):
|
||||
for key, value in additional_properties.items():
|
||||
if value is not None:
|
||||
options_dict[key] = value
|
||||
if (tool_choice := options_dict.get("tool_choice")) and len(tool_choice.keys()) == 1:
|
||||
options_dict["tool_choice"] = tool_choice["mode"]
|
||||
return options_dict
|
||||
# response format
|
||||
if chat_options.response_format:
|
||||
run_options["response_format"] = type_to_response_format_param(chat_options.response_format)
|
||||
|
||||
def _create_chat_response(self, response: ChatCompletion, chat_options: ChatOptions) -> "ChatResponse":
|
||||
"""Create a chat message content object from a choice."""
|
||||
# additional properties
|
||||
additional_options = {
|
||||
key: value for key, value in chat_options.additional_properties.items() if value is not None
|
||||
}
|
||||
if additional_options:
|
||||
run_options.update(additional_options)
|
||||
return run_options
|
||||
|
||||
def _parse_response_from_openai(self, response: ChatCompletion, chat_options: ChatOptions) -> "ChatResponse":
|
||||
"""Parse a response from OpenAI into a ChatResponse."""
|
||||
response_metadata = self._get_metadata_from_chat_response(response)
|
||||
messages: list[ChatMessage] = []
|
||||
finish_reason: FinishReason | None = None
|
||||
@@ -210,15 +230,15 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
if choice.finish_reason:
|
||||
finish_reason = FinishReason(value=choice.finish_reason)
|
||||
contents: list[Contents] = []
|
||||
if text_content := self._parse_text_from_choice(choice):
|
||||
if text_content := self._parse_text_from_openai(choice):
|
||||
contents.append(text_content)
|
||||
if parsed_tool_calls := [tool for tool in self._get_tool_calls_from_chat_choice(choice)]:
|
||||
if parsed_tool_calls := [tool for tool in self._parse_tool_calls_from_openai(choice)]:
|
||||
contents.extend(parsed_tool_calls)
|
||||
messages.append(ChatMessage(role="assistant", contents=contents))
|
||||
return ChatResponse(
|
||||
response_id=response.id,
|
||||
created_at=datetime.fromtimestamp(response.created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
usage_details=self._usage_details_from_openai(response.usage) if response.usage else None,
|
||||
usage_details=self._parse_usage_from_openai(response.usage) if response.usage else None,
|
||||
messages=messages,
|
||||
model_id=response.model,
|
||||
additional_properties=response_metadata,
|
||||
@@ -226,16 +246,16 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
response_format=chat_options.response_format,
|
||||
)
|
||||
|
||||
def _create_chat_response_update(
|
||||
def _parse_response_update_from_openai(
|
||||
self,
|
||||
chunk: ChatCompletionChunk,
|
||||
) -> ChatResponseUpdate:
|
||||
"""Create a streaming chat message content object from a choice."""
|
||||
"""Parse a streaming response update from OpenAI."""
|
||||
chunk_metadata = self._get_metadata_from_streaming_chat_response(chunk)
|
||||
if chunk.usage:
|
||||
return ChatResponseUpdate(
|
||||
role=Role.ASSISTANT,
|
||||
contents=[UsageContent(details=self._usage_details_from_openai(chunk.usage), raw_representation=chunk)],
|
||||
contents=[UsageContent(details=self._parse_usage_from_openai(chunk.usage), raw_representation=chunk)],
|
||||
model_id=chunk.model,
|
||||
additional_properties=chunk_metadata,
|
||||
response_id=chunk.id,
|
||||
@@ -245,11 +265,11 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
finish_reason: FinishReason | None = None
|
||||
for choice in chunk.choices:
|
||||
chunk_metadata.update(self._get_metadata_from_chat_choice(choice))
|
||||
contents.extend(self._get_tool_calls_from_chat_choice(choice))
|
||||
contents.extend(self._parse_tool_calls_from_openai(choice))
|
||||
if choice.finish_reason:
|
||||
finish_reason = FinishReason(value=choice.finish_reason)
|
||||
|
||||
if text_content := self._parse_text_from_choice(choice):
|
||||
if text_content := self._parse_text_from_openai(choice):
|
||||
contents.append(text_content)
|
||||
return ChatResponseUpdate(
|
||||
created_at=datetime.fromtimestamp(chunk.created, tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
|
||||
@@ -263,7 +283,7 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
message_id=chunk.id,
|
||||
)
|
||||
|
||||
def _usage_details_from_openai(self, usage: CompletionUsage) -> UsageDetails:
|
||||
def _parse_usage_from_openai(self, usage: CompletionUsage) -> UsageDetails:
|
||||
details = UsageDetails(
|
||||
input_token_count=usage.prompt_tokens,
|
||||
output_token_count=usage.completion_tokens,
|
||||
@@ -285,7 +305,7 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
details["prompt/cached_tokens"] = tokens
|
||||
return details
|
||||
|
||||
def _parse_text_from_choice(self, choice: Choice | ChunkChoice) -> TextContent | None:
|
||||
def _parse_text_from_openai(self, choice: Choice | ChunkChoice) -> TextContent | None:
|
||||
"""Parse the choice into a TextContent object."""
|
||||
message = choice.message if isinstance(choice, Choice) else choice.delta
|
||||
if message.content:
|
||||
@@ -312,8 +332,8 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
"logprobs": getattr(choice, "logprobs", None),
|
||||
}
|
||||
|
||||
def _get_tool_calls_from_chat_choice(self, choice: Choice | ChunkChoice) -> list[Contents]:
|
||||
"""Get tool calls from a chat choice."""
|
||||
def _parse_tool_calls_from_openai(self, choice: Choice | ChunkChoice) -> list[Contents]:
|
||||
"""Parse tool calls from an OpenAI response choice."""
|
||||
resp: list[Contents] = []
|
||||
content = choice.message if isinstance(choice, Choice) else choice.delta
|
||||
if content and content.tool_calls:
|
||||
@@ -331,13 +351,13 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
# When you enable asynchronous content filtering in Azure OpenAI, you may receive empty deltas
|
||||
return resp
|
||||
|
||||
def _prepare_chat_history_for_request(
|
||||
def _prepare_messages_for_openai(
|
||||
self,
|
||||
chat_messages: Sequence[ChatMessage],
|
||||
role_key: str = "role",
|
||||
content_key: str = "content",
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Prepare the chat history for a request.
|
||||
"""Prepare the chat history for an OpenAI request.
|
||||
|
||||
Allowing customization of the key names for role/author, and optionally overriding the role.
|
||||
|
||||
@@ -355,14 +375,14 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
Returns:
|
||||
prepared_chat_history (Any): The prepared chat history for a request.
|
||||
"""
|
||||
list_of_list = [self._openai_chat_message_parser(message) for message in chat_messages]
|
||||
list_of_list = [self._prepare_message_for_openai(message) for message in chat_messages]
|
||||
# Flatten the list of lists into a single list
|
||||
return list(chain.from_iterable(list_of_list))
|
||||
|
||||
# region Parsers
|
||||
|
||||
def _openai_chat_message_parser(self, message: ChatMessage) -> list[dict[str, Any]]:
|
||||
"""Parse a chat message into the openai format."""
|
||||
def _prepare_message_for_openai(self, message: ChatMessage) -> list[dict[str, Any]]:
|
||||
"""Prepare a chat message for OpenAI."""
|
||||
all_messages: list[dict[str, Any]] = []
|
||||
for content in message.contents:
|
||||
# Skip approval content - it's internal framework state, not for the LLM
|
||||
@@ -372,13 +392,15 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
args: dict[str, Any] = {
|
||||
"role": message.role.value if isinstance(message.role, Role) else message.role,
|
||||
}
|
||||
if message.author_name and message.role != Role.TOOL:
|
||||
args["name"] = message.author_name
|
||||
match content:
|
||||
case FunctionCallContent():
|
||||
if all_messages and "tool_calls" in all_messages[-1]:
|
||||
# If the last message already has tool calls, append to it
|
||||
all_messages[-1]["tool_calls"].append(self._openai_content_parser(content))
|
||||
all_messages[-1]["tool_calls"].append(self._prepare_content_for_openai(content))
|
||||
else:
|
||||
args["tool_calls"] = [self._openai_content_parser(content)] # type: ignore
|
||||
args["tool_calls"] = [self._prepare_content_for_openai(content)] # type: ignore
|
||||
case FunctionResultContent():
|
||||
args["tool_call_id"] = content.call_id
|
||||
if content.result is not None:
|
||||
@@ -387,13 +409,13 @@ class OpenAIBaseChatClient(OpenAIBase, BaseChatClient):
|
||||
if "content" not in args:
|
||||
args["content"] = []
|
||||
# this is a list to allow multi-modal content
|
||||
args["content"].append(self._openai_content_parser(content)) # type: ignore
|
||||
args["content"].append(self._prepare_content_for_openai(content)) # type: ignore
|
||||
if "content" in args or "tool_calls" in args:
|
||||
all_messages.append(args)
|
||||
return all_messages
|
||||
|
||||
def _openai_content_parser(self, content: Contents) -> dict[str, Any]:
|
||||
"""Parse contents into the openai format."""
|
||||
def _prepare_content_for_openai(self, content: Contents) -> dict[str, Any]:
|
||||
"""Prepare content for OpenAI."""
|
||||
match content:
|
||||
case FunctionCallContent():
|
||||
args = json.dumps(content.arguments) if isinstance(content.arguments, Mapping) else content.arguments
|
||||
|
||||
@@ -89,28 +89,16 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> ChatResponse:
|
||||
client = await self.ensure_client()
|
||||
run_options = await self.prepare_options(messages, chat_options, **kwargs)
|
||||
response_format = run_options.pop("response_format", None)
|
||||
text_config = run_options.pop("text", None)
|
||||
text_format, text_config = self._prepare_text_config(response_format=response_format, text_config=text_config)
|
||||
if text_config:
|
||||
run_options["text"] = text_config
|
||||
client = await self._ensure_client()
|
||||
# prepare
|
||||
run_options = await self._prepare_options(messages, chat_options, **kwargs)
|
||||
try:
|
||||
if not text_format:
|
||||
response = await client.responses.create(
|
||||
stream=False,
|
||||
**run_options,
|
||||
)
|
||||
chat_options.conversation_id = self.get_conversation_id(response, chat_options.store)
|
||||
return self._create_response_content(response, chat_options=chat_options)
|
||||
parsed_response: ParsedResponse[BaseModel] = await client.responses.parse(
|
||||
text_format=text_format,
|
||||
stream=False,
|
||||
**run_options,
|
||||
)
|
||||
chat_options.conversation_id = self.get_conversation_id(parsed_response, chat_options.store)
|
||||
return self._create_response_content(parsed_response, chat_options=chat_options)
|
||||
# execute and process
|
||||
if "text_format" in run_options:
|
||||
response = await client.responses.parse(stream=False, **run_options)
|
||||
else:
|
||||
response = await client.responses.create(stream=False, **run_options)
|
||||
return self._parse_response_from_openai(response, chat_options=chat_options)
|
||||
except BadRequestError as ex:
|
||||
if ex.code == "content_filter":
|
||||
raise OpenAIContentFilterException(
|
||||
@@ -134,35 +122,23 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable[ChatResponseUpdate]:
|
||||
client = await self.ensure_client()
|
||||
run_options = await self.prepare_options(messages, chat_options, **kwargs)
|
||||
client = await self._ensure_client()
|
||||
# prepare
|
||||
run_options = await self._prepare_options(messages, chat_options, **kwargs)
|
||||
function_call_ids: dict[int, tuple[str, str]] = {} # output_index: (call_id, name)
|
||||
response_format = run_options.pop("response_format", None)
|
||||
text_config = run_options.pop("text", None)
|
||||
text_format, text_config = self._prepare_text_config(response_format=response_format, text_config=text_config)
|
||||
if text_config:
|
||||
run_options["text"] = text_config
|
||||
try:
|
||||
if not text_format:
|
||||
response = await client.responses.create(
|
||||
stream=True,
|
||||
**run_options,
|
||||
)
|
||||
async for chunk in response:
|
||||
update = self._create_streaming_response_content(
|
||||
# execute and process
|
||||
if "text_format" not in run_options:
|
||||
async for chunk in await client.responses.create(stream=True, **run_options):
|
||||
yield self._parse_chunk_from_openai(
|
||||
chunk, chat_options=chat_options, function_call_ids=function_call_ids
|
||||
)
|
||||
yield update
|
||||
return
|
||||
async with client.responses.stream(
|
||||
text_format=text_format,
|
||||
**run_options,
|
||||
) as response:
|
||||
async with client.responses.stream(**run_options) as response:
|
||||
async for chunk in response:
|
||||
update = self._create_streaming_response_content(
|
||||
yield self._parse_chunk_from_openai(
|
||||
chunk, chat_options=chat_options, function_call_ids=function_call_ids
|
||||
)
|
||||
yield update
|
||||
except BadRequestError as ex:
|
||||
if ex.code == "content_filter":
|
||||
raise OpenAIContentFilterException(
|
||||
@@ -179,33 +155,33 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
inner_exception=ex,
|
||||
) from ex
|
||||
|
||||
def _prepare_text_config(
|
||||
def _prepare_response_and_text_format(
|
||||
self,
|
||||
*,
|
||||
response_format: Any,
|
||||
text_config: MutableMapping[str, Any] | None,
|
||||
) -> tuple[type[BaseModel] | None, dict[str, Any] | None]:
|
||||
"""Normalize response_format into Responses text configuration and parse target."""
|
||||
prepared_text = dict(text_config) if isinstance(text_config, MutableMapping) else None
|
||||
if text_config is not None and not isinstance(text_config, MutableMapping):
|
||||
raise ServiceInvalidRequestError("text must be a mapping when provided.")
|
||||
text_config = cast(dict[str, Any], text_config) if isinstance(text_config, MutableMapping) else None
|
||||
|
||||
if response_format is None:
|
||||
return None, prepared_text
|
||||
return None, text_config
|
||||
|
||||
if isinstance(response_format, type) and issubclass(response_format, BaseModel):
|
||||
if prepared_text and "format" in prepared_text:
|
||||
if text_config and "format" in text_config:
|
||||
raise ServiceInvalidRequestError("response_format cannot be combined with explicit text.format.")
|
||||
return response_format, prepared_text
|
||||
return response_format, text_config
|
||||
|
||||
if isinstance(response_format, Mapping):
|
||||
format_config = self._convert_response_format(cast("Mapping[str, Any]", response_format))
|
||||
if prepared_text is None:
|
||||
prepared_text = {}
|
||||
elif "format" in prepared_text and prepared_text["format"] != format_config:
|
||||
if text_config is None:
|
||||
text_config = {}
|
||||
elif "format" in text_config and text_config["format"] != format_config:
|
||||
raise ServiceInvalidRequestError("Conflicting response_format definitions detected.")
|
||||
prepared_text["format"] = format_config
|
||||
return None, prepared_text
|
||||
text_config["format"] = format_config
|
||||
return None, text_config
|
||||
|
||||
raise ServiceInvalidRequestError("response_format must be a Pydantic model or mapping.")
|
||||
|
||||
@@ -245,23 +221,33 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
|
||||
raise ServiceInvalidRequestError("Unsupported response_format provided for Responses client.")
|
||||
|
||||
def get_conversation_id(
|
||||
def _get_conversation_id(
|
||||
self, response: OpenAIResponse | ParsedResponse[BaseModel], store: bool | None
|
||||
) -> str | None:
|
||||
"""Get the conversation ID from the response if store is True."""
|
||||
return None if store is False else response.id
|
||||
if store is False:
|
||||
return None
|
||||
# If conversation ID exists, it means that we operate with conversation
|
||||
# so we use conversation ID as input and output.
|
||||
if response.conversation and response.conversation.id:
|
||||
return response.conversation.id
|
||||
# If conversation ID doesn't exist, we operate with responses
|
||||
# so we use response ID as input and output.
|
||||
return response.id
|
||||
|
||||
# region Prep methods
|
||||
|
||||
def _tools_to_response_tools(
|
||||
self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]]
|
||||
def _prepare_tools_for_openai(
|
||||
self, tools: Sequence[ToolProtocol | MutableMapping[str, Any]] | None
|
||||
) -> list[ToolParam | dict[str, Any]]:
|
||||
response_tools: list[ToolParam | dict[str, Any]] = []
|
||||
if not tools:
|
||||
return response_tools
|
||||
for tool in tools:
|
||||
if isinstance(tool, ToolProtocol):
|
||||
match tool:
|
||||
case HostedMCPTool():
|
||||
response_tools.append(self.get_mcp_tool(tool))
|
||||
response_tools.append(self._prepare_mcp_tool(tool))
|
||||
case HostedCodeInterpreterTool():
|
||||
tool_args: CodeInterpreterContainerCodeInterpreterToolAuto = {"type": "auto"}
|
||||
if tool.inputs:
|
||||
@@ -363,7 +349,8 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
response_tools.append(tool_dict)
|
||||
return response_tools
|
||||
|
||||
def get_mcp_tool(self, tool: HostedMCPTool) -> Any:
|
||||
@staticmethod
|
||||
def _prepare_mcp_tool(tool: HostedMCPTool) -> Mcp:
|
||||
"""Get MCP tool from HostedMCPTool."""
|
||||
mcp: Mcp = {
|
||||
"type": "mcp",
|
||||
@@ -386,18 +373,13 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
|
||||
return mcp
|
||||
|
||||
async def prepare_options(
|
||||
async def _prepare_options(
|
||||
self,
|
||||
messages: MutableSequence[ChatMessage],
|
||||
chat_options: ChatOptions,
|
||||
**kwargs: Any,
|
||||
) -> dict[str, Any]:
|
||||
"""Take ChatOptions and create the specific options for Responses API."""
|
||||
conversation_id = kwargs.pop("conversation_id", None)
|
||||
|
||||
if conversation_id:
|
||||
chat_options.conversation_id = conversation_id
|
||||
|
||||
run_options: dict[str, Any] = chat_options.to_dict(
|
||||
exclude={
|
||||
"type",
|
||||
@@ -407,12 +389,24 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
"seed", # not supported
|
||||
"stop", # not supported
|
||||
"instructions", # already added as system message
|
||||
"response_format", # handled separately
|
||||
"conversation_id", # handled separately
|
||||
"additional_properties", # handled separately
|
||||
}
|
||||
)
|
||||
# messages
|
||||
request_input = self._prepare_messages_for_openai(messages)
|
||||
if not request_input:
|
||||
raise ServiceInvalidRequestError("Messages are required for chat completions")
|
||||
run_options["input"] = request_input
|
||||
|
||||
if chat_options.response_format:
|
||||
run_options["response_format"] = chat_options.response_format
|
||||
# model id
|
||||
if not run_options.get("model"):
|
||||
if not self.model_id:
|
||||
raise ValueError("model_id must be a non-empty string")
|
||||
run_options["model"] = self.model_id
|
||||
|
||||
# translations between ChatOptions and Responses API
|
||||
translations = {
|
||||
"model_id": "model",
|
||||
"allow_multiple_tool_calls": "parallel_tool_calls",
|
||||
@@ -423,34 +417,53 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
if old_key in run_options and old_key != new_key:
|
||||
run_options[new_key] = run_options.pop(old_key)
|
||||
|
||||
# Handle different conversation ID formats
|
||||
if conversation_id := self._get_current_conversation_id(chat_options, **kwargs):
|
||||
if conversation_id.startswith("resp_"):
|
||||
# For response IDs, set previous_response_id and remove conversation property
|
||||
run_options["previous_response_id"] = conversation_id
|
||||
elif conversation_id.startswith("conv_"):
|
||||
# For conversation IDs, set conversation and remove previous_response_id property
|
||||
run_options["conversation"] = conversation_id
|
||||
else:
|
||||
# If the format is unrecognized, default to previous_response_id
|
||||
run_options["previous_response_id"] = conversation_id
|
||||
|
||||
# tools
|
||||
if chat_options.tools is None:
|
||||
run_options.pop("parallel_tool_calls", None)
|
||||
if tools := self._prepare_tools_for_openai(chat_options.tools):
|
||||
run_options["tools"] = tools
|
||||
else:
|
||||
run_options["tools"] = self._tools_to_response_tools(chat_options.tools)
|
||||
|
||||
# model id
|
||||
if not run_options.get("model"):
|
||||
if not self.model_id:
|
||||
raise ValueError("model_id must be a non-empty string")
|
||||
run_options["model"] = self.model_id
|
||||
|
||||
# messages
|
||||
request_input = self._prepare_chat_messages_for_request(messages)
|
||||
if not request_input:
|
||||
raise ServiceInvalidRequestError("Messages are required for chat completions")
|
||||
run_options["input"] = request_input
|
||||
|
||||
# additional provider specific settings
|
||||
if additional_properties := run_options.pop("additional_properties", None):
|
||||
for key, value in additional_properties.items():
|
||||
if value is not None:
|
||||
run_options[key] = value
|
||||
run_options.pop("parallel_tool_calls", None)
|
||||
run_options.pop("tool_choice", None)
|
||||
# tool choice when `tool_choice` is a dict with single key `mode`, extract the mode value
|
||||
if (tool_choice := run_options.get("tool_choice")) and len(tool_choice.keys()) == 1:
|
||||
run_options["tool_choice"] = tool_choice["mode"]
|
||||
|
||||
# additional properties
|
||||
additional_options = {
|
||||
key: value for key, value in chat_options.additional_properties.items() if value is not None
|
||||
}
|
||||
if additional_options:
|
||||
run_options.update(additional_options)
|
||||
|
||||
# response format and text config (after additional_properties so user can pass text via additional_properties)
|
||||
response_format = chat_options.response_format
|
||||
text_config = run_options.pop("text", None)
|
||||
response_format, text_config = self._prepare_response_and_text_format(
|
||||
response_format=response_format, text_config=text_config
|
||||
)
|
||||
if text_config:
|
||||
run_options["text"] = text_config
|
||||
if response_format:
|
||||
run_options["text_format"] = response_format
|
||||
|
||||
return run_options
|
||||
|
||||
def _prepare_chat_messages_for_request(self, chat_messages: Sequence[ChatMessage]) -> list[dict[str, Any]]:
|
||||
def _get_current_conversation_id(self, chat_options: ChatOptions, **kwargs: Any) -> str | None:
|
||||
"""Get the current conversation ID from chat options or kwargs."""
|
||||
return chat_options.conversation_id or kwargs.get("conversation_id")
|
||||
|
||||
def _prepare_messages_for_openai(self, chat_messages: Sequence[ChatMessage]) -> list[dict[str, Any]]:
|
||||
"""Prepare the chat messages for a request.
|
||||
|
||||
Allowing customization of the key names for role/author, and optionally overriding the role.
|
||||
@@ -476,16 +489,16 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
and "fc_id" in content.additional_properties
|
||||
):
|
||||
call_id_to_id[content.call_id] = content.additional_properties["fc_id"]
|
||||
list_of_list = [self._openai_chat_message_parser(message, call_id_to_id) for message in chat_messages]
|
||||
list_of_list = [self._prepare_message_for_openai(message, call_id_to_id) for message in chat_messages]
|
||||
# Flatten the list of lists into a single list
|
||||
return list(chain.from_iterable(list_of_list))
|
||||
|
||||
def _openai_chat_message_parser(
|
||||
def _prepare_message_for_openai(
|
||||
self,
|
||||
message: ChatMessage,
|
||||
call_id_to_id: dict[str, str],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Parse a chat message into the openai format."""
|
||||
"""Prepare a chat message for the OpenAI Responses API format."""
|
||||
all_messages: list[dict[str, Any]] = []
|
||||
args: dict[str, Any] = {
|
||||
"role": message.role.value if isinstance(message.role, Role) else message.role,
|
||||
@@ -497,28 +510,28 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
continue
|
||||
case FunctionResultContent():
|
||||
new_args: dict[str, Any] = {}
|
||||
new_args.update(self._openai_content_parser(message.role, content, call_id_to_id))
|
||||
new_args.update(self._prepare_content_for_openai(message.role, content, call_id_to_id))
|
||||
all_messages.append(new_args)
|
||||
case FunctionCallContent():
|
||||
function_call = self._openai_content_parser(message.role, content, call_id_to_id)
|
||||
function_call = self._prepare_content_for_openai(message.role, content, call_id_to_id)
|
||||
all_messages.append(function_call) # type: ignore
|
||||
case FunctionApprovalResponseContent() | FunctionApprovalRequestContent():
|
||||
all_messages.append(self._openai_content_parser(message.role, content, call_id_to_id)) # type: ignore
|
||||
all_messages.append(self._prepare_content_for_openai(message.role, content, call_id_to_id)) # type: ignore
|
||||
case _:
|
||||
if "content" not in args:
|
||||
args["content"] = []
|
||||
args["content"].append(self._openai_content_parser(message.role, content, call_id_to_id)) # type: ignore
|
||||
args["content"].append(self._prepare_content_for_openai(message.role, content, call_id_to_id)) # type: ignore
|
||||
if "content" in args or "tool_calls" in args:
|
||||
all_messages.append(args)
|
||||
return all_messages
|
||||
|
||||
def _openai_content_parser(
|
||||
def _prepare_content_for_openai(
|
||||
self,
|
||||
role: Role,
|
||||
content: Contents,
|
||||
call_id_to_id: dict[str, str],
|
||||
) -> dict[str, Any]:
|
||||
"""Parse contents into the openai format."""
|
||||
"""Prepare content for the OpenAI Responses API format."""
|
||||
match content:
|
||||
case TextContent():
|
||||
return {
|
||||
@@ -625,14 +638,13 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
logger.debug("Unsupported content type passed (type: %s)", type(content))
|
||||
return {}
|
||||
|
||||
# region Response creation methods
|
||||
|
||||
def _create_response_content(
|
||||
# region Parse methods
|
||||
def _parse_response_from_openai(
|
||||
self,
|
||||
response: OpenAIResponse | ParsedResponse[BaseModel],
|
||||
chat_options: ChatOptions,
|
||||
) -> "ChatResponse":
|
||||
"""Create a chat message content object from a choice."""
|
||||
"""Parse an OpenAI Responses API response into a ChatResponse."""
|
||||
structured_response: BaseModel | None = response.output_parsed if isinstance(response, ParsedResponse) else None # type: ignore[reportUnknownMemberType]
|
||||
|
||||
metadata: dict[str, Any] = response.metadata or {}
|
||||
@@ -826,11 +838,9 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
"raw_representation": response,
|
||||
}
|
||||
|
||||
conversation_id = self.get_conversation_id(response, chat_options.store) # type: ignore[reportArgumentType]
|
||||
|
||||
if conversation_id:
|
||||
if conversation_id := self._get_conversation_id(response, chat_options.store):
|
||||
args["conversation_id"] = conversation_id
|
||||
if response.usage and (usage_details := self._usage_details_from_openai(response.usage)):
|
||||
if response.usage and (usage_details := self._parse_usage_from_openai(response.usage)):
|
||||
args["usage_details"] = usage_details
|
||||
if structured_response:
|
||||
args["value"] = structured_response
|
||||
@@ -838,13 +848,13 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
args["response_format"] = chat_options.response_format
|
||||
return ChatResponse(**args)
|
||||
|
||||
def _create_streaming_response_content(
|
||||
def _parse_chunk_from_openai(
|
||||
self,
|
||||
event: OpenAIResponseStreamEvent,
|
||||
chat_options: ChatOptions,
|
||||
function_call_ids: dict[int, tuple[str, str]],
|
||||
) -> ChatResponseUpdate:
|
||||
"""Create a streaming chat message content object from a choice."""
|
||||
"""Parse an OpenAI Responses API streaming event into a ChatResponseUpdate."""
|
||||
metadata: dict[str, Any] = {}
|
||||
contents: list[Contents] = []
|
||||
conversation_id: str | None = None
|
||||
@@ -931,10 +941,10 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
contents.append(TextReasoningContent(text=event.text, raw_representation=event))
|
||||
metadata.update(self._get_metadata_from_response(event))
|
||||
case "response.completed":
|
||||
conversation_id = self.get_conversation_id(event.response, chat_options.store)
|
||||
conversation_id = self._get_conversation_id(event.response, chat_options.store)
|
||||
model = event.response.model
|
||||
if event.response.usage:
|
||||
usage = self._usage_details_from_openai(event.response.usage)
|
||||
usage = self._parse_usage_from_openai(event.response.usage)
|
||||
if usage:
|
||||
contents.append(UsageContent(details=usage, raw_representation=event))
|
||||
case "response.output_item.added":
|
||||
@@ -1102,7 +1112,7 @@ class OpenAIBaseResponsesClient(OpenAIBase, BaseChatClient):
|
||||
raw_representation=event,
|
||||
)
|
||||
|
||||
def _usage_details_from_openai(self, usage: ResponseUsage) -> UsageDetails | None:
|
||||
def _parse_usage_from_openai(self, usage: ResponseUsage) -> UsageDetails | None:
|
||||
details = UsageDetails(
|
||||
input_token_count=usage.input_tokens,
|
||||
output_token_count=usage.output_tokens,
|
||||
|
||||
@@ -160,16 +160,16 @@ class OpenAIBase(SerializationMixin):
|
||||
for key, value in kwargs.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
async def initialize_client(self) -> None:
|
||||
async def _initialize_client(self) -> None:
|
||||
"""Initialize OpenAI client asynchronously.
|
||||
|
||||
Override in subclasses to initialize the OpenAI client asynchronously.
|
||||
"""
|
||||
pass
|
||||
|
||||
async def ensure_client(self) -> AsyncOpenAI:
|
||||
async def _ensure_client(self) -> AsyncOpenAI:
|
||||
"""Ensure OpenAI client is initialized."""
|
||||
await self.initialize_client()
|
||||
await self._initialize_client()
|
||||
if self.client is None:
|
||||
raise ServiceInitializationError("OpenAI client is not initialized")
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ async def test_cmc(
|
||||
mock_create.assert_awaited_once_with(
|
||||
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
|
||||
stream=False,
|
||||
messages=azure_chat_client._prepare_chat_history_for_request(chat_history), # type: ignore
|
||||
messages=azure_chat_client._prepare_messages_for_openai(chat_history), # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ async def test_cmc_with_logit_bias(
|
||||
|
||||
mock_create.assert_awaited_once_with(
|
||||
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
|
||||
messages=azure_chat_client._prepare_chat_history_for_request(chat_history), # type: ignore
|
||||
messages=azure_chat_client._prepare_messages_for_openai(chat_history), # type: ignore
|
||||
stream=False,
|
||||
logit_bias=token_bias,
|
||||
)
|
||||
@@ -241,7 +241,7 @@ async def test_cmc_with_stop(
|
||||
|
||||
mock_create.assert_awaited_once_with(
|
||||
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
|
||||
messages=azure_chat_client._prepare_chat_history_for_request(chat_history), # type: ignore
|
||||
messages=azure_chat_client._prepare_messages_for_openai(chat_history), # type: ignore
|
||||
stream=False,
|
||||
stop=stop,
|
||||
)
|
||||
@@ -311,7 +311,7 @@ async def test_azure_on_your_data(
|
||||
|
||||
mock_create.assert_awaited_once_with(
|
||||
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
|
||||
messages=azure_chat_client._prepare_chat_history_for_request(messages_out), # type: ignore
|
||||
messages=azure_chat_client._prepare_messages_for_openai(messages_out), # type: ignore
|
||||
stream=False,
|
||||
extra_body=expected_data_settings,
|
||||
)
|
||||
@@ -381,7 +381,7 @@ async def test_azure_on_your_data_string(
|
||||
|
||||
mock_create.assert_awaited_once_with(
|
||||
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
|
||||
messages=azure_chat_client._prepare_chat_history_for_request(messages_out), # type: ignore
|
||||
messages=azure_chat_client._prepare_messages_for_openai(messages_out), # type: ignore
|
||||
stream=False,
|
||||
extra_body=expected_data_settings,
|
||||
)
|
||||
@@ -438,7 +438,7 @@ async def test_azure_on_your_data_fail(
|
||||
|
||||
mock_create.assert_awaited_once_with(
|
||||
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
|
||||
messages=azure_chat_client._prepare_chat_history_for_request(messages_out), # type: ignore
|
||||
messages=azure_chat_client._prepare_messages_for_openai(messages_out), # type: ignore
|
||||
stream=False,
|
||||
extra_body=expected_data_settings,
|
||||
)
|
||||
@@ -584,7 +584,7 @@ async def test_get_streaming(
|
||||
mock_create.assert_awaited_once_with(
|
||||
model=azure_openai_unit_test_env["AZURE_OPENAI_CHAT_DEPLOYMENT_NAME"],
|
||||
stream=True,
|
||||
messages=azure_chat_client._prepare_chat_history_for_request(chat_history), # type: ignore
|
||||
messages=azure_chat_client._prepare_messages_for_openai(chat_history), # type: ignore
|
||||
# NOTE: The `stream_options={"include_usage": True}` is explicitly enforced in
|
||||
# `OpenAIChatCompletionBase._inner_get_streaming_response`.
|
||||
# To ensure consistency, we align the arguments here accordingly.
|
||||
|
||||
@@ -24,14 +24,14 @@ from agent_framework import (
|
||||
)
|
||||
from agent_framework._mcp import (
|
||||
MCPTool,
|
||||
_ai_content_to_mcp_types,
|
||||
_chat_message_to_mcp_types,
|
||||
_get_input_model_from_mcp_prompt,
|
||||
_get_input_model_from_mcp_tool,
|
||||
_mcp_call_tool_result_to_ai_contents,
|
||||
_mcp_prompt_message_to_chat_message,
|
||||
_mcp_type_to_ai_content,
|
||||
_normalize_mcp_name,
|
||||
_parse_content_from_mcp,
|
||||
_parse_contents_from_mcp_tool_result,
|
||||
_parse_message_from_mcp,
|
||||
_prepare_content_for_mcp,
|
||||
_prepare_message_for_mcp,
|
||||
)
|
||||
from agent_framework.exceptions import ToolException, ToolExecutionException
|
||||
|
||||
@@ -60,7 +60,7 @@ def test_normalize_mcp_name():
|
||||
def test_mcp_prompt_message_to_ai_content():
|
||||
"""Test conversion from MCP prompt message to AI content."""
|
||||
mcp_message = types.PromptMessage(role="user", content=types.TextContent(type="text", text="Hello, world!"))
|
||||
ai_content = _mcp_prompt_message_to_chat_message(mcp_message)
|
||||
ai_content = _parse_message_from_mcp(mcp_message)
|
||||
|
||||
assert isinstance(ai_content, ChatMessage)
|
||||
assert ai_content.role.value == "user"
|
||||
@@ -70,7 +70,7 @@ def test_mcp_prompt_message_to_ai_content():
|
||||
assert ai_content.raw_representation == mcp_message
|
||||
|
||||
|
||||
def test_mcp_call_tool_result_to_ai_contents():
|
||||
def test_parse_contents_from_mcp_tool_result():
|
||||
"""Test conversion from MCP tool result to AI contents."""
|
||||
mcp_result = types.CallToolResult(
|
||||
content=[
|
||||
@@ -79,7 +79,7 @@ def test_mcp_call_tool_result_to_ai_contents():
|
||||
types.ImageContent(type="image", data=b"abc", mimeType="image/webp"),
|
||||
]
|
||||
)
|
||||
ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)
|
||||
ai_contents = _parse_contents_from_mcp_tool_result(mcp_result)
|
||||
|
||||
assert len(ai_contents) == 3
|
||||
assert isinstance(ai_contents[0], TextContent)
|
||||
@@ -100,7 +100,7 @@ def test_mcp_call_tool_result_with_meta_error():
|
||||
_meta={"isError": True, "errorCode": "TOOL_ERROR", "errorMessage": "Tool execution failed"},
|
||||
)
|
||||
|
||||
ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)
|
||||
ai_contents = _parse_contents_from_mcp_tool_result(mcp_result)
|
||||
|
||||
assert len(ai_contents) == 1
|
||||
assert isinstance(ai_contents[0], TextContent)
|
||||
@@ -131,7 +131,7 @@ def test_mcp_call_tool_result_with_meta_arbitrary_data():
|
||||
},
|
||||
)
|
||||
|
||||
ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)
|
||||
ai_contents = _parse_contents_from_mcp_tool_result(mcp_result)
|
||||
|
||||
assert len(ai_contents) == 1
|
||||
assert isinstance(ai_contents[0], TextContent)
|
||||
@@ -153,7 +153,7 @@ def test_mcp_call_tool_result_with_meta_merging_existing_properties():
|
||||
text_content = types.TextContent(type="text", text="Test content")
|
||||
mcp_result = types.CallToolResult(content=[text_content], _meta={"newField": "newValue", "isError": False})
|
||||
|
||||
ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)
|
||||
ai_contents = _parse_contents_from_mcp_tool_result(mcp_result)
|
||||
|
||||
assert len(ai_contents) == 1
|
||||
content = ai_contents[0]
|
||||
@@ -169,7 +169,7 @@ def test_mcp_call_tool_result_with_meta_none():
|
||||
mcp_result = types.CallToolResult(content=[types.TextContent(type="text", text="No meta test")])
|
||||
# No _meta field set
|
||||
|
||||
ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)
|
||||
ai_contents = _parse_contents_from_mcp_tool_result(mcp_result)
|
||||
|
||||
assert len(ai_contents) == 1
|
||||
assert isinstance(ai_contents[0], TextContent)
|
||||
@@ -191,7 +191,7 @@ def test_mcp_call_tool_result_regression_successful_workflow():
|
||||
]
|
||||
)
|
||||
|
||||
ai_contents = _mcp_call_tool_result_to_ai_contents(mcp_result)
|
||||
ai_contents = _parse_contents_from_mcp_tool_result(mcp_result)
|
||||
|
||||
# Verify basic conversion still works correctly
|
||||
assert len(ai_contents) == 2
|
||||
@@ -213,7 +213,7 @@ def test_mcp_call_tool_result_regression_successful_workflow():
|
||||
def test_mcp_content_types_to_ai_content_text():
|
||||
"""Test conversion of MCP text content to AI content."""
|
||||
mcp_content = types.TextContent(type="text", text="Sample text")
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
|
||||
ai_content = _parse_content_from_mcp(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, TextContent)
|
||||
assert ai_content.text == "Sample text"
|
||||
@@ -224,7 +224,7 @@ def test_mcp_content_types_to_ai_content_image():
|
||||
"""Test conversion of MCP image content to AI content."""
|
||||
mcp_content = types.ImageContent(type="image", data="abc", mimeType="image/jpeg")
|
||||
mcp_content = types.ImageContent(type="image", data=b"abc", mimeType="image/jpeg")
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
|
||||
ai_content = _parse_content_from_mcp(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, DataContent)
|
||||
assert ai_content.uri == "data:image/jpeg;base64,abc"
|
||||
@@ -235,7 +235,7 @@ def test_mcp_content_types_to_ai_content_image():
|
||||
def test_mcp_content_types_to_ai_content_audio():
|
||||
"""Test conversion of MCP audio content to AI content."""
|
||||
mcp_content = types.AudioContent(type="audio", data="def", mimeType="audio/wav")
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
|
||||
ai_content = _parse_content_from_mcp(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, DataContent)
|
||||
assert ai_content.uri == "data:audio/wav;base64,def"
|
||||
@@ -251,7 +251,7 @@ def test_mcp_content_types_to_ai_content_resource_link():
|
||||
name="test_resource",
|
||||
mimeType="application/json",
|
||||
)
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
|
||||
ai_content = _parse_content_from_mcp(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, UriContent)
|
||||
assert ai_content.uri == "https://example.com/resource"
|
||||
@@ -267,7 +267,7 @@ def test_mcp_content_types_to_ai_content_embedded_resource_text():
|
||||
text="Embedded text content",
|
||||
)
|
||||
mcp_content = types.EmbeddedResource(type="resource", resource=text_resource)
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
|
||||
ai_content = _parse_content_from_mcp(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, TextContent)
|
||||
assert ai_content.text == "Embedded text content"
|
||||
@@ -283,7 +283,7 @@ def test_mcp_content_types_to_ai_content_embedded_resource_blob():
|
||||
blob="data:application/octet-stream;base64,dGVzdCBkYXRh",
|
||||
)
|
||||
mcp_content = types.EmbeddedResource(type="resource", resource=blob_resource)
|
||||
ai_content = _mcp_type_to_ai_content(mcp_content)[0]
|
||||
ai_content = _parse_content_from_mcp(mcp_content)[0]
|
||||
|
||||
assert isinstance(ai_content, DataContent)
|
||||
assert ai_content.uri == "data:application/octet-stream;base64,dGVzdCBkYXRh"
|
||||
@@ -294,7 +294,7 @@ def test_mcp_content_types_to_ai_content_embedded_resource_blob():
|
||||
def test_ai_content_to_mcp_content_types_text():
|
||||
"""Test conversion of AI text content to MCP content."""
|
||||
ai_content = TextContent(text="Sample text")
|
||||
mcp_content = _ai_content_to_mcp_types(ai_content)
|
||||
mcp_content = _prepare_content_for_mcp(ai_content)
|
||||
|
||||
assert isinstance(mcp_content, types.TextContent)
|
||||
assert mcp_content.type == "text"
|
||||
@@ -304,7 +304,7 @@ def test_ai_content_to_mcp_content_types_text():
|
||||
def test_ai_content_to_mcp_content_types_data_image():
|
||||
"""Test conversion of AI data content to MCP content."""
|
||||
ai_content = DataContent(uri="data:image/png;base64,xyz", media_type="image/png")
|
||||
mcp_content = _ai_content_to_mcp_types(ai_content)
|
||||
mcp_content = _prepare_content_for_mcp(ai_content)
|
||||
|
||||
assert isinstance(mcp_content, types.ImageContent)
|
||||
assert mcp_content.type == "image"
|
||||
@@ -315,7 +315,7 @@ def test_ai_content_to_mcp_content_types_data_image():
|
||||
def test_ai_content_to_mcp_content_types_data_audio():
|
||||
"""Test conversion of AI data content to MCP content."""
|
||||
ai_content = DataContent(uri="data:audio/mpeg;base64,xyz", media_type="audio/mpeg")
|
||||
mcp_content = _ai_content_to_mcp_types(ai_content)
|
||||
mcp_content = _prepare_content_for_mcp(ai_content)
|
||||
|
||||
assert isinstance(mcp_content, types.AudioContent)
|
||||
assert mcp_content.type == "audio"
|
||||
@@ -329,7 +329,7 @@ def test_ai_content_to_mcp_content_types_data_binary():
|
||||
uri="data:application/octet-stream;base64,xyz",
|
||||
media_type="application/octet-stream",
|
||||
)
|
||||
mcp_content = _ai_content_to_mcp_types(ai_content)
|
||||
mcp_content = _prepare_content_for_mcp(ai_content)
|
||||
|
||||
assert isinstance(mcp_content, types.EmbeddedResource)
|
||||
assert mcp_content.type == "resource"
|
||||
@@ -340,7 +340,7 @@ def test_ai_content_to_mcp_content_types_data_binary():
|
||||
def test_ai_content_to_mcp_content_types_uri():
|
||||
"""Test conversion of AI URI content to MCP content."""
|
||||
ai_content = UriContent(uri="https://example.com/resource", media_type="application/json")
|
||||
mcp_content = _ai_content_to_mcp_types(ai_content)
|
||||
mcp_content = _prepare_content_for_mcp(ai_content)
|
||||
|
||||
assert isinstance(mcp_content, types.ResourceLink)
|
||||
assert mcp_content.type == "resource_link"
|
||||
@@ -348,7 +348,7 @@ def test_ai_content_to_mcp_content_types_uri():
|
||||
assert mcp_content.mimeType == "application/json"
|
||||
|
||||
|
||||
def test_chat_message_to_mcp_types():
|
||||
def test_prepare_message_for_mcp():
|
||||
message = ChatMessage(
|
||||
role="user",
|
||||
contents=[
|
||||
@@ -356,7 +356,7 @@ def test_chat_message_to_mcp_types():
|
||||
DataContent(uri="data:image/png;base64,xyz", media_type="image/png"),
|
||||
],
|
||||
)
|
||||
mcp_contents = _chat_message_to_mcp_types(message)
|
||||
mcp_contents = _prepare_message_for_mcp(message)
|
||||
assert len(mcp_contents) == 2
|
||||
assert isinstance(mcp_contents[0], types.TextContent)
|
||||
assert isinstance(mcp_contents[1], types.ImageContent)
|
||||
|
||||
@@ -463,9 +463,9 @@ async def test_openai_assistants_client_process_stream_events_requires_action(mo
|
||||
"""Test _process_stream_events with thread.run.requires_action event."""
|
||||
chat_client = create_test_openai_assistants_client(mock_async_openai)
|
||||
|
||||
# Mock the _create_function_call_contents method to return test content
|
||||
# Mock the _parse_function_calls_from_assistants method to return test content
|
||||
test_function_content = FunctionCallContent(call_id="call-123", name="test_func", arguments={"arg": "value"})
|
||||
chat_client._create_function_call_contents = MagicMock(return_value=[test_function_content]) # type: ignore
|
||||
chat_client._parse_function_calls_from_assistants = MagicMock(return_value=[test_function_content]) # type: ignore
|
||||
|
||||
# Create a mock Run object
|
||||
mock_run = MagicMock(spec=Run)
|
||||
@@ -498,8 +498,8 @@ async def test_openai_assistants_client_process_stream_events_requires_action(mo
|
||||
assert update.contents[0] == test_function_content
|
||||
assert update.raw_representation == mock_run
|
||||
|
||||
# Verify _create_function_call_contents was called correctly
|
||||
chat_client._create_function_call_contents.assert_called_once_with(mock_run, None) # type: ignore
|
||||
# Verify _parse_function_calls_from_assistants was called correctly
|
||||
chat_client._parse_function_calls_from_assistants.assert_called_once_with(mock_run, None) # type: ignore
|
||||
|
||||
|
||||
async def test_openai_assistants_client_process_stream_events_run_step_created(mock_async_openai: MagicMock) -> None:
|
||||
@@ -585,8 +585,8 @@ async def test_openai_assistants_client_process_stream_events_run_completed_with
|
||||
assert update.raw_representation == mock_run
|
||||
|
||||
|
||||
def test_openai_assistants_client_create_function_call_contents_basic(mock_async_openai: MagicMock) -> None:
|
||||
"""Test _create_function_call_contents with a simple function call."""
|
||||
def test_openai_assistants_client_parse_function_calls_from_assistants_basic(mock_async_openai: MagicMock) -> None:
|
||||
"""Test _parse_function_calls_from_assistants with a simple function call."""
|
||||
|
||||
chat_client = create_test_openai_assistants_client(mock_async_openai)
|
||||
|
||||
@@ -605,7 +605,7 @@ def test_openai_assistants_client_create_function_call_contents_basic(mock_async
|
||||
|
||||
# Call the method
|
||||
response_id = "response_456"
|
||||
contents = chat_client._create_function_call_contents(mock_run, response_id) # type: ignore
|
||||
contents = chat_client._parse_function_calls_from_assistants(mock_run, response_id) # type: ignore
|
||||
|
||||
# Test that one function call content was created
|
||||
assert len(contents) == 1
|
||||
@@ -825,24 +825,24 @@ def test_openai_assistants_client_prepare_options_with_image_content(mock_async_
|
||||
assert message["content"][0]["image_url"]["url"] == "https://example.com/image.jpg"
|
||||
|
||||
|
||||
def test_openai_assistants_client_convert_function_results_to_tool_output_empty(mock_async_openai: MagicMock) -> None:
|
||||
"""Test _convert_function_results_to_tool_output with empty list."""
|
||||
def test_openai_assistants_client_prepare_tool_outputs_for_assistants_empty(mock_async_openai: MagicMock) -> None:
|
||||
"""Test _prepare_tool_outputs_for_assistants with empty list."""
|
||||
chat_client = create_test_openai_assistants_client(mock_async_openai)
|
||||
|
||||
run_id, tool_outputs = chat_client._convert_function_results_to_tool_output([]) # type: ignore
|
||||
run_id, tool_outputs = chat_client._prepare_tool_outputs_for_assistants([]) # type: ignore
|
||||
|
||||
assert run_id is None
|
||||
assert tool_outputs is None
|
||||
|
||||
|
||||
def test_openai_assistants_client_convert_function_results_to_tool_output_valid(mock_async_openai: MagicMock) -> None:
|
||||
"""Test _convert_function_results_to_tool_output with valid function results."""
|
||||
def test_openai_assistants_client_prepare_tool_outputs_for_assistants_valid(mock_async_openai: MagicMock) -> None:
|
||||
"""Test _prepare_tool_outputs_for_assistants with valid function results."""
|
||||
chat_client = create_test_openai_assistants_client(mock_async_openai)
|
||||
|
||||
call_id = json.dumps(["run-123", "call-456"])
|
||||
function_result = FunctionResultContent(call_id=call_id, result="Function executed successfully")
|
||||
|
||||
run_id, tool_outputs = chat_client._convert_function_results_to_tool_output([function_result]) # type: ignore
|
||||
run_id, tool_outputs = chat_client._prepare_tool_outputs_for_assistants([function_result]) # type: ignore
|
||||
|
||||
assert run_id == "run-123"
|
||||
assert tool_outputs is not None
|
||||
@@ -851,10 +851,10 @@ def test_openai_assistants_client_convert_function_results_to_tool_output_valid(
|
||||
assert tool_outputs[0].get("output") == "Function executed successfully"
|
||||
|
||||
|
||||
def test_openai_assistants_client_convert_function_results_to_tool_output_mismatched_run_ids(
|
||||
def test_openai_assistants_client_prepare_tool_outputs_for_assistants_mismatched_run_ids(
|
||||
mock_async_openai: MagicMock,
|
||||
) -> None:
|
||||
"""Test _convert_function_results_to_tool_output with mismatched run IDs."""
|
||||
"""Test _prepare_tool_outputs_for_assistants with mismatched run IDs."""
|
||||
chat_client = create_test_openai_assistants_client(mock_async_openai)
|
||||
|
||||
# Create function results with different run IDs
|
||||
@@ -863,7 +863,7 @@ def test_openai_assistants_client_convert_function_results_to_tool_output_mismat
|
||||
function_result1 = FunctionResultContent(call_id=call_id1, result="Result 1")
|
||||
function_result2 = FunctionResultContent(call_id=call_id2, result="Result 2")
|
||||
|
||||
run_id, tool_outputs = chat_client._convert_function_results_to_tool_output([function_result1, function_result2]) # type: ignore
|
||||
run_id, tool_outputs = chat_client._prepare_tool_outputs_for_assistants([function_result1, function_result2]) # type: ignore
|
||||
|
||||
# Should only process the first one since run IDs don't match
|
||||
assert run_id == "run-123"
|
||||
|
||||
@@ -182,12 +182,12 @@ def test_unsupported_tool_handling(openai_unit_test_env: dict[str, str]) -> None
|
||||
unsupported_tool.__class__.__name__ = "UnsupportedAITool"
|
||||
|
||||
# This should ignore the unsupported ToolProtocol and return empty list
|
||||
result = client._chat_to_tool_spec([unsupported_tool]) # type: ignore
|
||||
result = client._prepare_tools_for_openai([unsupported_tool]) # type: ignore
|
||||
assert result == []
|
||||
|
||||
# Also test with a non-ToolProtocol that should be converted to dict
|
||||
dict_tool = {"type": "function", "name": "test"}
|
||||
result = client._chat_to_tool_spec([dict_tool]) # type: ignore
|
||||
result = client._prepare_tools_for_openai([dict_tool]) # type: ignore
|
||||
assert result == [dict_tool]
|
||||
|
||||
|
||||
@@ -637,7 +637,7 @@ def test_chat_response_content_order_text_before_tool_calls(openai_unit_test_env
|
||||
)
|
||||
|
||||
client = OpenAIChatClient()
|
||||
response = client._create_chat_response(mock_response, ChatOptions())
|
||||
response = client._parse_response_from_openai(mock_response, ChatOptions())
|
||||
|
||||
# Verify we have both text and tool call content
|
||||
assert len(response.messages) == 1
|
||||
@@ -658,7 +658,7 @@ def test_function_result_falsy_values_handling(openai_unit_test_env: dict[str, s
|
||||
# Test with empty list (falsy but not None)
|
||||
message_with_empty_list = ChatMessage(role="tool", contents=[FunctionResultContent(call_id="call-123", result=[])])
|
||||
|
||||
openai_messages = client._openai_chat_message_parser(message_with_empty_list)
|
||||
openai_messages = client._prepare_message_for_openai(message_with_empty_list)
|
||||
assert len(openai_messages) == 1
|
||||
assert openai_messages[0]["content"] == "[]" # Empty list should be JSON serialized
|
||||
|
||||
@@ -667,14 +667,14 @@ def test_function_result_falsy_values_handling(openai_unit_test_env: dict[str, s
|
||||
role="tool", contents=[FunctionResultContent(call_id="call-456", result="")]
|
||||
)
|
||||
|
||||
openai_messages = client._openai_chat_message_parser(message_with_empty_string)
|
||||
openai_messages = client._prepare_message_for_openai(message_with_empty_string)
|
||||
assert len(openai_messages) == 1
|
||||
assert openai_messages[0]["content"] == "" # Empty string should be preserved
|
||||
|
||||
# Test with False (falsy but not None)
|
||||
message_with_false = ChatMessage(role="tool", contents=[FunctionResultContent(call_id="call-789", result=False)])
|
||||
|
||||
openai_messages = client._openai_chat_message_parser(message_with_false)
|
||||
openai_messages = client._prepare_message_for_openai(message_with_false)
|
||||
assert len(openai_messages) == 1
|
||||
assert openai_messages[0]["content"] == "false" # False should be JSON serialized
|
||||
|
||||
@@ -695,7 +695,7 @@ def test_function_result_exception_handling(openai_unit_test_env: dict[str, str]
|
||||
],
|
||||
)
|
||||
|
||||
openai_messages = client._openai_chat_message_parser(message_with_exception)
|
||||
openai_messages = client._prepare_message_for_openai(message_with_exception)
|
||||
assert len(openai_messages) == 1
|
||||
assert openai_messages[0]["content"] == "Error: Function failed."
|
||||
assert openai_messages[0]["tool_call_id"] == "call-123"
|
||||
@@ -708,8 +708,8 @@ def test_prepare_function_call_results_string_passthrough():
|
||||
assert isinstance(result, str)
|
||||
|
||||
|
||||
def test_openai_content_parser_data_content_image(openai_unit_test_env: dict[str, str]) -> None:
|
||||
"""Test _openai_content_parser converts DataContent with image media type to OpenAI format."""
|
||||
def test_prepare_content_for_openai_data_content_image(openai_unit_test_env: dict[str, str]) -> None:
|
||||
"""Test _prepare_content_for_openai converts DataContent with image media type to OpenAI format."""
|
||||
client = OpenAIChatClient()
|
||||
|
||||
# Test DataContent with image media type
|
||||
@@ -718,7 +718,7 @@ def test_openai_content_parser_data_content_image(openai_unit_test_env: dict[str
|
||||
media_type="image/png",
|
||||
)
|
||||
|
||||
result = client._openai_content_parser(image_data_content) # type: ignore
|
||||
result = client._prepare_content_for_openai(image_data_content) # type: ignore
|
||||
|
||||
# Should convert to OpenAI image_url format
|
||||
assert result["type"] == "image_url"
|
||||
@@ -727,7 +727,7 @@ def test_openai_content_parser_data_content_image(openai_unit_test_env: dict[str
|
||||
# Test DataContent with non-image media type should use default model_dump
|
||||
text_data_content = DataContent(uri="data:text/plain;base64,SGVsbG8gV29ybGQ=", media_type="text/plain")
|
||||
|
||||
result = client._openai_content_parser(text_data_content) # type: ignore
|
||||
result = client._prepare_content_for_openai(text_data_content) # type: ignore
|
||||
|
||||
# Should use default model_dump format
|
||||
assert result["type"] == "data"
|
||||
@@ -740,7 +740,7 @@ def test_openai_content_parser_data_content_image(openai_unit_test_env: dict[str
|
||||
media_type="audio/wav",
|
||||
)
|
||||
|
||||
result = client._openai_content_parser(audio_data_content) # type: ignore
|
||||
result = client._prepare_content_for_openai(audio_data_content) # type: ignore
|
||||
|
||||
# Should convert to OpenAI input_audio format
|
||||
assert result["type"] == "input_audio"
|
||||
@@ -751,7 +751,7 @@ def test_openai_content_parser_data_content_image(openai_unit_test_env: dict[str
|
||||
# Test DataContent with MP3 audio
|
||||
mp3_data_content = DataContent(uri="data:audio/mp3;base64,//uQAAAAWGluZwAAAA8AAAACAAACcQ==", media_type="audio/mp3")
|
||||
|
||||
result = client._openai_content_parser(mp3_data_content) # type: ignore
|
||||
result = client._prepare_content_for_openai(mp3_data_content) # type: ignore
|
||||
|
||||
# Should convert to OpenAI input_audio format with mp3
|
||||
assert result["type"] == "input_audio"
|
||||
@@ -760,8 +760,8 @@ def test_openai_content_parser_data_content_image(openai_unit_test_env: dict[str
|
||||
assert result["input_audio"]["format"] == "mp3"
|
||||
|
||||
|
||||
def test_openai_content_parser_document_file_mapping(openai_unit_test_env: dict[str, str]) -> None:
|
||||
"""Test _openai_content_parser converts document files (PDF, DOCX, etc.) to OpenAI file format."""
|
||||
def test_prepare_content_for_openai_document_file_mapping(openai_unit_test_env: dict[str, str]) -> None:
|
||||
"""Test _prepare_content_for_openai converts document files (PDF, DOCX, etc.) to OpenAI file format."""
|
||||
client = OpenAIChatClient()
|
||||
|
||||
# Test PDF without filename - should omit filename in OpenAI payload
|
||||
@@ -770,7 +770,7 @@ def test_openai_content_parser_document_file_mapping(openai_unit_test_env: dict[
|
||||
media_type="application/pdf",
|
||||
)
|
||||
|
||||
result = client._openai_content_parser(pdf_data_content) # type: ignore
|
||||
result = client._prepare_content_for_openai(pdf_data_content) # type: ignore
|
||||
|
||||
# Should convert to OpenAI file format without filename
|
||||
assert result["type"] == "file"
|
||||
@@ -787,7 +787,7 @@ def test_openai_content_parser_document_file_mapping(openai_unit_test_env: dict[
|
||||
additional_properties={"filename": "report.pdf"},
|
||||
)
|
||||
|
||||
result = client._openai_content_parser(pdf_with_filename) # type: ignore
|
||||
result = client._prepare_content_for_openai(pdf_with_filename) # type: ignore
|
||||
|
||||
# Should use custom filename
|
||||
assert result["type"] == "file"
|
||||
@@ -820,7 +820,7 @@ def test_openai_content_parser_document_file_mapping(openai_unit_test_env: dict[
|
||||
media_type=case["media_type"],
|
||||
)
|
||||
|
||||
result = client._openai_content_parser(doc_content) # type: ignore
|
||||
result = client._prepare_content_for_openai(doc_content) # type: ignore
|
||||
|
||||
# All application/* types should now be mapped to file format
|
||||
assert result["type"] == "file"
|
||||
@@ -834,7 +834,7 @@ def test_openai_content_parser_document_file_mapping(openai_unit_test_env: dict[
|
||||
additional_properties={"filename": case["filename"]},
|
||||
)
|
||||
|
||||
result = client._openai_content_parser(doc_with_filename) # type: ignore
|
||||
result = client._prepare_content_for_openai(doc_with_filename) # type: ignore
|
||||
|
||||
# Should now use file format with filename
|
||||
assert result["type"] == "file"
|
||||
@@ -848,7 +848,7 @@ def test_openai_content_parser_document_file_mapping(openai_unit_test_env: dict[
|
||||
additional_properties={},
|
||||
)
|
||||
|
||||
result = client._openai_content_parser(pdf_empty_props) # type: ignore
|
||||
result = client._prepare_content_for_openai(pdf_empty_props) # type: ignore
|
||||
|
||||
assert result["type"] == "file"
|
||||
assert "filename" not in result["file"]
|
||||
@@ -860,7 +860,7 @@ def test_openai_content_parser_document_file_mapping(openai_unit_test_env: dict[
|
||||
additional_properties={"filename": None},
|
||||
)
|
||||
|
||||
result = client._openai_content_parser(pdf_none_filename) # type: ignore
|
||||
result = client._prepare_content_for_openai(pdf_none_filename) # type: ignore
|
||||
|
||||
assert result["type"] == "file"
|
||||
assert "filename" not in result["file"] # None filename should be omitted
|
||||
|
||||
@@ -76,7 +76,7 @@ async def test_cmc(
|
||||
mock_create.assert_awaited_once_with(
|
||||
model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
|
||||
stream=False,
|
||||
messages=openai_chat_completion._prepare_chat_history_for_request(chat_history), # type: ignore
|
||||
messages=openai_chat_completion._prepare_messages_for_openai(chat_history), # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ async def test_cmc_chat_options(
|
||||
mock_create.assert_awaited_once_with(
|
||||
model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
|
||||
stream=False,
|
||||
messages=openai_chat_completion._prepare_chat_history_for_request(chat_history), # type: ignore
|
||||
messages=openai_chat_completion._prepare_messages_for_openai(chat_history), # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ async def test_cmc_no_fcc_in_response(
|
||||
mock_create.assert_awaited_once_with(
|
||||
model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
|
||||
stream=False,
|
||||
messages=openai_chat_completion._prepare_chat_history_for_request(orig_chat_history), # type: ignore
|
||||
messages=openai_chat_completion._prepare_messages_for_openai(orig_chat_history), # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ async def test_scmc_chat_options(
|
||||
model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
|
||||
stream=True,
|
||||
stream_options={"include_usage": True},
|
||||
messages=openai_chat_completion._prepare_chat_history_for_request(chat_history), # type: ignore
|
||||
messages=openai_chat_completion._prepare_messages_for_openai(chat_history), # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ async def test_cmc_additional_properties(
|
||||
mock_create.assert_awaited_once_with(
|
||||
model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
|
||||
stream=False,
|
||||
messages=openai_chat_completion._prepare_chat_history_for_request(chat_history), # type: ignore
|
||||
messages=openai_chat_completion._prepare_messages_for_openai(chat_history), # type: ignore
|
||||
reasoning_effort="low",
|
||||
)
|
||||
|
||||
@@ -246,7 +246,7 @@ async def test_get_streaming(
|
||||
model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
|
||||
stream=True,
|
||||
stream_options={"include_usage": True},
|
||||
messages=openai_chat_completion._prepare_chat_history_for_request(orig_chat_history), # type: ignore
|
||||
messages=openai_chat_completion._prepare_messages_for_openai(orig_chat_history), # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@@ -285,7 +285,7 @@ async def test_get_streaming_singular(
|
||||
model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
|
||||
stream=True,
|
||||
stream_options={"include_usage": True},
|
||||
messages=openai_chat_completion._prepare_chat_history_for_request(orig_chat_history), # type: ignore
|
||||
messages=openai_chat_completion._prepare_messages_for_openai(orig_chat_history), # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@@ -349,7 +349,7 @@ async def test_get_streaming_no_fcc_in_response(
|
||||
model=openai_unit_test_env["OPENAI_CHAT_MODEL_ID"],
|
||||
stream=True,
|
||||
stream_options={"include_usage": True},
|
||||
messages=openai_chat_completion._prepare_chat_history_for_request(orig_chat_history), # type: ignore
|
||||
messages=openai_chat_completion._prepare_messages_for_openai(orig_chat_history), # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@@ -399,7 +399,7 @@ def test_chat_response_created_at_uses_utc(openai_unit_test_env: dict[str, str])
|
||||
)
|
||||
|
||||
client = OpenAIChatClient()
|
||||
response = client._create_chat_response(mock_response, ChatOptions())
|
||||
response = client._parse_response_from_openai(mock_response, ChatOptions())
|
||||
|
||||
# Verify that created_at is correctly formatted as UTC
|
||||
assert response.created_at is not None
|
||||
@@ -431,7 +431,7 @@ def test_chat_response_update_created_at_uses_utc(openai_unit_test_env: dict[str
|
||||
)
|
||||
|
||||
client = OpenAIChatClient()
|
||||
response_update = client._create_chat_response_update(mock_chunk)
|
||||
response_update = client._parse_response_update_from_openai(mock_chunk)
|
||||
|
||||
# Verify that created_at is correctly formatted as UTC
|
||||
assert response_update.created_at is not None
|
||||
|
||||
@@ -368,6 +368,7 @@ async def test_response_format_parse_path() -> None:
|
||||
mock_parsed_response.output_parsed = None
|
||||
mock_parsed_response.usage = None
|
||||
mock_parsed_response.finish_reason = None
|
||||
mock_parsed_response.conversation = None # No conversation object
|
||||
|
||||
with patch.object(client.client.responses, "parse", return_value=mock_parsed_response):
|
||||
response = await client.get_response(
|
||||
@@ -454,7 +455,7 @@ async def test_get_streaming_response_with_all_parameters() -> None:
|
||||
|
||||
|
||||
def test_response_content_creation_with_annotations() -> None:
|
||||
"""Test _create_response_content with different annotation types."""
|
||||
"""Test _parse_response_from_openai with different annotation types."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Create a mock response with annotated text content
|
||||
@@ -485,7 +486,7 @@ def test_response_content_creation_with_annotations() -> None:
|
||||
mock_response.output = [mock_message_item]
|
||||
|
||||
with patch.object(client, "_get_metadata_from_response", return_value={}):
|
||||
response = client._create_response_content(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
response = client._parse_response_from_openai(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
|
||||
assert len(response.messages[0].contents) >= 1
|
||||
assert isinstance(response.messages[0].contents[0], TextContent)
|
||||
@@ -494,7 +495,7 @@ def test_response_content_creation_with_annotations() -> None:
|
||||
|
||||
|
||||
def test_response_content_creation_with_refusal() -> None:
|
||||
"""Test _create_response_content with refusal content."""
|
||||
"""Test _parse_response_from_openai with refusal content."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Create a mock response with refusal content
|
||||
@@ -516,7 +517,7 @@ def test_response_content_creation_with_refusal() -> None:
|
||||
|
||||
mock_response.output = [mock_message_item]
|
||||
|
||||
response = client._create_response_content(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
response = client._parse_response_from_openai(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
|
||||
assert len(response.messages[0].contents) == 1
|
||||
assert isinstance(response.messages[0].contents[0], TextContent)
|
||||
@@ -524,7 +525,7 @@ def test_response_content_creation_with_refusal() -> None:
|
||||
|
||||
|
||||
def test_response_content_creation_with_reasoning() -> None:
|
||||
"""Test _create_response_content with reasoning content."""
|
||||
"""Test _parse_response_from_openai with reasoning content."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Create a mock response with reasoning content
|
||||
@@ -546,7 +547,7 @@ def test_response_content_creation_with_reasoning() -> None:
|
||||
|
||||
mock_response.output = [mock_reasoning_item]
|
||||
|
||||
response = client._create_response_content(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
response = client._parse_response_from_openai(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
|
||||
assert len(response.messages[0].contents) == 2
|
||||
assert isinstance(response.messages[0].contents[0], TextReasoningContent)
|
||||
@@ -554,7 +555,7 @@ def test_response_content_creation_with_reasoning() -> None:
|
||||
|
||||
|
||||
def test_response_content_creation_with_code_interpreter() -> None:
|
||||
"""Test _create_response_content with code interpreter outputs."""
|
||||
"""Test _parse_response_from_openai with code interpreter outputs."""
|
||||
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
@@ -582,7 +583,7 @@ def test_response_content_creation_with_code_interpreter() -> None:
|
||||
|
||||
mock_response.output = [mock_code_interpreter_item]
|
||||
|
||||
response = client._create_response_content(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
response = client._parse_response_from_openai(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
|
||||
assert len(response.messages[0].contents) == 2
|
||||
assert isinstance(response.messages[0].contents[0], TextContent)
|
||||
@@ -593,7 +594,7 @@ def test_response_content_creation_with_code_interpreter() -> None:
|
||||
|
||||
|
||||
def test_response_content_creation_with_function_call() -> None:
|
||||
"""Test _create_response_content with function call content."""
|
||||
"""Test _parse_response_from_openai with function call content."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Create a mock response with function call
|
||||
@@ -614,7 +615,7 @@ def test_response_content_creation_with_function_call() -> None:
|
||||
|
||||
mock_response.output = [mock_function_call_item]
|
||||
|
||||
response = client._create_response_content(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
response = client._parse_response_from_openai(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
|
||||
assert len(response.messages[0].contents) == 1
|
||||
assert isinstance(response.messages[0].contents[0], FunctionCallContent)
|
||||
@@ -624,7 +625,7 @@ def test_response_content_creation_with_function_call() -> None:
|
||||
assert function_call.arguments == '{"location": "Seattle"}'
|
||||
|
||||
|
||||
def test_tools_to_response_tools_with_hosted_mcp() -> None:
|
||||
def test_prepare_tools_for_openai_with_hosted_mcp() -> None:
|
||||
"""Test that HostedMCPTool is converted to the correct response tool dict."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
@@ -638,7 +639,7 @@ def test_tools_to_response_tools_with_hosted_mcp() -> None:
|
||||
additional_properties={"custom": "value"},
|
||||
)
|
||||
|
||||
resp_tools = client._tools_to_response_tools([tool])
|
||||
resp_tools = client._prepare_tools_for_openai([tool])
|
||||
assert isinstance(resp_tools, list)
|
||||
assert len(resp_tools) == 1
|
||||
mcp = resp_tools[0]
|
||||
@@ -654,7 +655,7 @@ def test_tools_to_response_tools_with_hosted_mcp() -> None:
|
||||
assert "require_approval" in mcp
|
||||
|
||||
|
||||
def test_create_response_content_with_mcp_approval_request() -> None:
|
||||
def test_parse_response_from_openai_with_mcp_approval_request() -> None:
|
||||
"""Test that a non-streaming mcp_approval_request is parsed into FunctionApprovalRequestContent."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
@@ -675,7 +676,7 @@ def test_create_response_content_with_mcp_approval_request() -> None:
|
||||
|
||||
mock_response.output = [mock_item]
|
||||
|
||||
response = client._create_response_content(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
response = client._parse_response_from_openai(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
|
||||
assert isinstance(response.messages[0].contents[0], FunctionApprovalRequestContent)
|
||||
req = response.messages[0].contents[0]
|
||||
@@ -716,7 +717,7 @@ def test_responses_client_created_at_uses_utc(openai_unit_test_env: dict[str, st
|
||||
mock_response.output = [mock_message_item]
|
||||
|
||||
with patch.object(client, "_get_metadata_from_response", return_value={}):
|
||||
response = client._create_response_content(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
response = client._parse_response_from_openai(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
|
||||
# Verify that created_at is correctly formatted as UTC
|
||||
assert response.created_at is not None
|
||||
@@ -730,7 +731,7 @@ def test_responses_client_created_at_uses_utc(openai_unit_test_env: dict[str, st
|
||||
)
|
||||
|
||||
|
||||
def test_tools_to_response_tools_with_raw_image_generation() -> None:
|
||||
def test_prepare_tools_for_openai_with_raw_image_generation() -> None:
|
||||
"""Test that raw image_generation tool dict is handled correctly with parameter mapping."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
@@ -744,7 +745,7 @@ def test_tools_to_response_tools_with_raw_image_generation() -> None:
|
||||
"background": "transparent",
|
||||
}
|
||||
|
||||
resp_tools = client._tools_to_response_tools([tool])
|
||||
resp_tools = client._prepare_tools_for_openai([tool])
|
||||
assert isinstance(resp_tools, list)
|
||||
assert len(resp_tools) == 1
|
||||
|
||||
@@ -759,7 +760,7 @@ def test_tools_to_response_tools_with_raw_image_generation() -> None:
|
||||
assert image_tool["output_compression"] == 75
|
||||
|
||||
|
||||
def test_tools_to_response_tools_with_raw_image_generation_openai_responses_params() -> None:
|
||||
def test_prepare_tools_for_openai_with_raw_image_generation_openai_responses_params() -> None:
|
||||
"""Test raw image_generation tool with OpenAI-specific parameters."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
@@ -773,7 +774,7 @@ def test_tools_to_response_tools_with_raw_image_generation_openai_responses_para
|
||||
"partial_images": 2, # Should be integer 0-3
|
||||
}
|
||||
|
||||
resp_tools = client._tools_to_response_tools([tool])
|
||||
resp_tools = client._prepare_tools_for_openai([tool])
|
||||
assert isinstance(resp_tools, list)
|
||||
assert len(resp_tools) == 1
|
||||
|
||||
@@ -791,14 +792,14 @@ def test_tools_to_response_tools_with_raw_image_generation_openai_responses_para
|
||||
assert tool_dict["partial_images"] == 2
|
||||
|
||||
|
||||
def test_tools_to_response_tools_with_raw_image_generation_minimal() -> None:
|
||||
def test_prepare_tools_for_openai_with_raw_image_generation_minimal() -> None:
|
||||
"""Test raw image_generation tool with minimal configuration."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Test with minimal parameters (just type)
|
||||
tool = {"type": "image_generation"}
|
||||
|
||||
resp_tools = client._tools_to_response_tools([tool])
|
||||
resp_tools = client._prepare_tools_for_openai([tool])
|
||||
assert isinstance(resp_tools, list)
|
||||
assert len(resp_tools) == 1
|
||||
|
||||
@@ -809,7 +810,7 @@ def test_tools_to_response_tools_with_raw_image_generation_minimal() -> None:
|
||||
assert len(image_tool) == 1
|
||||
|
||||
|
||||
def test_create_streaming_response_content_with_mcp_approval_request() -> None:
|
||||
def test_parse_chunk_from_openai_with_mcp_approval_request() -> None:
|
||||
"""Test that a streaming mcp_approval_request event is parsed into FunctionApprovalRequestContent."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
chat_options = ChatOptions()
|
||||
@@ -825,7 +826,7 @@ def test_create_streaming_response_content_with_mcp_approval_request() -> None:
|
||||
mock_item.server_label = "My_MCP"
|
||||
mock_event.item = mock_item
|
||||
|
||||
update = client._create_streaming_response_content(mock_event, chat_options, function_call_ids)
|
||||
update = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids)
|
||||
assert any(isinstance(c, FunctionApprovalRequestContent) for c in update.contents)
|
||||
fa = next(c for c in update.contents if isinstance(c, FunctionApprovalRequestContent))
|
||||
assert fa.id == "approval-stream-1"
|
||||
@@ -901,7 +902,7 @@ async def test_end_to_end_mcp_approval_flow(span_exporter) -> None:
|
||||
|
||||
|
||||
def test_usage_details_basic() -> None:
|
||||
"""Test _usage_details_from_openai without cached or reasoning tokens."""
|
||||
"""Test _parse_usage_from_openai without cached or reasoning tokens."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
mock_usage = MagicMock()
|
||||
@@ -911,7 +912,7 @@ def test_usage_details_basic() -> None:
|
||||
mock_usage.input_tokens_details = None
|
||||
mock_usage.output_tokens_details = None
|
||||
|
||||
details = client._usage_details_from_openai(mock_usage) # type: ignore
|
||||
details = client._parse_usage_from_openai(mock_usage) # type: ignore
|
||||
assert details is not None
|
||||
assert details.input_token_count == 100
|
||||
assert details.output_token_count == 50
|
||||
@@ -919,7 +920,7 @@ def test_usage_details_basic() -> None:
|
||||
|
||||
|
||||
def test_usage_details_with_cached_tokens() -> None:
|
||||
"""Test _usage_details_from_openai with cached input tokens."""
|
||||
"""Test _parse_usage_from_openai with cached input tokens."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
mock_usage = MagicMock()
|
||||
@@ -930,14 +931,14 @@ def test_usage_details_with_cached_tokens() -> None:
|
||||
mock_usage.input_tokens_details.cached_tokens = 25
|
||||
mock_usage.output_tokens_details = None
|
||||
|
||||
details = client._usage_details_from_openai(mock_usage) # type: ignore
|
||||
details = client._parse_usage_from_openai(mock_usage) # type: ignore
|
||||
assert details is not None
|
||||
assert details.input_token_count == 200
|
||||
assert details.additional_counts["openai.cached_input_tokens"] == 25
|
||||
|
||||
|
||||
def test_usage_details_with_reasoning_tokens() -> None:
|
||||
"""Test _usage_details_from_openai with reasoning tokens."""
|
||||
"""Test _parse_usage_from_openai with reasoning tokens."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
mock_usage = MagicMock()
|
||||
@@ -948,7 +949,7 @@ def test_usage_details_with_reasoning_tokens() -> None:
|
||||
mock_usage.output_tokens_details = MagicMock()
|
||||
mock_usage.output_tokens_details.reasoning_tokens = 30
|
||||
|
||||
details = client._usage_details_from_openai(mock_usage) # type: ignore
|
||||
details = client._parse_usage_from_openai(mock_usage) # type: ignore
|
||||
assert details is not None
|
||||
assert details.output_token_count == 80
|
||||
assert details.additional_counts["openai.reasoning_tokens"] == 30
|
||||
@@ -975,7 +976,7 @@ def test_get_metadata_from_response() -> None:
|
||||
|
||||
|
||||
def test_streaming_response_basic_structure() -> None:
|
||||
"""Test that _create_streaming_response_content returns proper structure."""
|
||||
"""Test that _parse_chunk_from_openai returns proper structure."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
chat_options = ChatOptions(store=True)
|
||||
function_call_ids: dict[int, tuple[str, str]] = {}
|
||||
@@ -983,7 +984,7 @@ def test_streaming_response_basic_structure() -> None:
|
||||
# Test with a basic mock event to ensure the method returns proper structure
|
||||
mock_event = MagicMock()
|
||||
|
||||
response = client._create_streaming_response_content(mock_event, chat_options, function_call_ids) # type: ignore
|
||||
response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids) # type: ignore
|
||||
|
||||
# Should get a valid ChatResponseUpdate structure
|
||||
assert isinstance(response, ChatResponseUpdate)
|
||||
@@ -1008,7 +1009,7 @@ def test_streaming_annotation_added_with_file_path() -> None:
|
||||
"index": 42,
|
||||
}
|
||||
|
||||
response = client._create_streaming_response_content(mock_event, chat_options, function_call_ids)
|
||||
response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids)
|
||||
|
||||
assert len(response.contents) == 1
|
||||
content = response.contents[0]
|
||||
@@ -1035,7 +1036,7 @@ def test_streaming_annotation_added_with_file_citation() -> None:
|
||||
"index": 15,
|
||||
}
|
||||
|
||||
response = client._create_streaming_response_content(mock_event, chat_options, function_call_ids)
|
||||
response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids)
|
||||
|
||||
assert len(response.contents) == 1
|
||||
content = response.contents[0]
|
||||
@@ -1064,7 +1065,7 @@ def test_streaming_annotation_added_with_container_file_citation() -> None:
|
||||
"end_index": 50,
|
||||
}
|
||||
|
||||
response = client._create_streaming_response_content(mock_event, chat_options, function_call_ids)
|
||||
response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids)
|
||||
|
||||
assert len(response.contents) == 1
|
||||
content = response.contents[0]
|
||||
@@ -1091,7 +1092,7 @@ def test_streaming_annotation_added_with_unknown_type() -> None:
|
||||
"url": "https://example.com",
|
||||
}
|
||||
|
||||
response = client._create_streaming_response_content(mock_event, chat_options, function_call_ids)
|
||||
response = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids)
|
||||
|
||||
# url_citation should not produce HostedFileContent
|
||||
assert len(response.contents) == 0
|
||||
@@ -1137,8 +1138,8 @@ def test_get_streaming_response_with_response_format() -> None:
|
||||
asyncio.run(run_streaming())
|
||||
|
||||
|
||||
def test_openai_content_parser_image_content() -> None:
|
||||
"""Test _openai_content_parser with image content variations."""
|
||||
def test_prepare_content_for_openai_image_content() -> None:
|
||||
"""Test _prepare_content_for_openai with image content variations."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Test image content with detail parameter and file_id
|
||||
@@ -1147,7 +1148,7 @@ def test_openai_content_parser_image_content() -> None:
|
||||
media_type="image/jpeg",
|
||||
additional_properties={"detail": "high", "file_id": "file_123"},
|
||||
)
|
||||
result = client._openai_content_parser(Role.USER, image_content_with_detail, {}) # type: ignore
|
||||
result = client._prepare_content_for_openai(Role.USER, image_content_with_detail, {}) # type: ignore
|
||||
assert result["type"] == "input_image"
|
||||
assert result["image_url"] == "https://example.com/image.jpg"
|
||||
assert result["detail"] == "high"
|
||||
@@ -1155,47 +1156,47 @@ def test_openai_content_parser_image_content() -> None:
|
||||
|
||||
# Test image content without additional properties (defaults)
|
||||
image_content_basic = UriContent(uri="https://example.com/basic.png", media_type="image/png")
|
||||
result = client._openai_content_parser(Role.USER, image_content_basic, {}) # type: ignore
|
||||
result = client._prepare_content_for_openai(Role.USER, image_content_basic, {}) # type: ignore
|
||||
assert result["type"] == "input_image"
|
||||
assert result["detail"] == "auto"
|
||||
assert result["file_id"] is None
|
||||
|
||||
|
||||
def test_openai_content_parser_audio_content() -> None:
|
||||
"""Test _openai_content_parser with audio content variations."""
|
||||
def test_prepare_content_for_openai_audio_content() -> None:
|
||||
"""Test _prepare_content_for_openai with audio content variations."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Test WAV audio content
|
||||
wav_content = UriContent(uri="data:audio/wav;base64,abc123", media_type="audio/wav")
|
||||
result = client._openai_content_parser(Role.USER, wav_content, {}) # type: ignore
|
||||
result = client._prepare_content_for_openai(Role.USER, wav_content, {}) # type: ignore
|
||||
assert result["type"] == "input_audio"
|
||||
assert result["input_audio"]["data"] == "data:audio/wav;base64,abc123"
|
||||
assert result["input_audio"]["format"] == "wav"
|
||||
|
||||
# Test MP3 audio content
|
||||
mp3_content = UriContent(uri="data:audio/mp3;base64,def456", media_type="audio/mp3")
|
||||
result = client._openai_content_parser(Role.USER, mp3_content, {}) # type: ignore
|
||||
result = client._prepare_content_for_openai(Role.USER, mp3_content, {}) # type: ignore
|
||||
assert result["type"] == "input_audio"
|
||||
assert result["input_audio"]["format"] == "mp3"
|
||||
|
||||
|
||||
def test_openai_content_parser_unsupported_content() -> None:
|
||||
"""Test _openai_content_parser with unsupported content types."""
|
||||
def test_prepare_content_for_openai_unsupported_content() -> None:
|
||||
"""Test _prepare_content_for_openai with unsupported content types."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Test unsupported audio format
|
||||
unsupported_audio = UriContent(uri="data:audio/ogg;base64,ghi789", media_type="audio/ogg")
|
||||
result = client._openai_content_parser(Role.USER, unsupported_audio, {}) # type: ignore
|
||||
result = client._prepare_content_for_openai(Role.USER, unsupported_audio, {}) # type: ignore
|
||||
assert result == {}
|
||||
|
||||
# Test non-media content
|
||||
text_uri_content = UriContent(uri="https://example.com/document.txt", media_type="text/plain")
|
||||
result = client._openai_content_parser(Role.USER, text_uri_content, {}) # type: ignore
|
||||
result = client._prepare_content_for_openai(Role.USER, text_uri_content, {}) # type: ignore
|
||||
assert result == {}
|
||||
|
||||
|
||||
def test_create_streaming_response_content_code_interpreter() -> None:
|
||||
"""Test _create_streaming_response_content with code_interpreter_call."""
|
||||
def test_parse_chunk_from_openai_code_interpreter() -> None:
|
||||
"""Test _parse_chunk_from_openai with code_interpreter_call."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
chat_options = ChatOptions()
|
||||
function_call_ids: dict[int, tuple[str, str]] = {}
|
||||
@@ -1211,15 +1212,15 @@ def test_create_streaming_response_content_code_interpreter() -> None:
|
||||
mock_item_image.code = None
|
||||
mock_event_image.item = mock_item_image
|
||||
|
||||
result = client._create_streaming_response_content(mock_event_image, chat_options, function_call_ids) # type: ignore
|
||||
result = client._parse_chunk_from_openai(mock_event_image, chat_options, function_call_ids) # type: ignore
|
||||
assert len(result.contents) == 1
|
||||
assert isinstance(result.contents[0], UriContent)
|
||||
assert result.contents[0].uri == "https://example.com/plot.png"
|
||||
assert result.contents[0].media_type == "image"
|
||||
|
||||
|
||||
def test_create_streaming_response_content_reasoning() -> None:
|
||||
"""Test _create_streaming_response_content with reasoning content."""
|
||||
def test_parse_chunk_from_openai_reasoning() -> None:
|
||||
"""Test _parse_chunk_from_openai with reasoning content."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
chat_options = ChatOptions()
|
||||
function_call_ids: dict[int, tuple[str, str]] = {}
|
||||
@@ -1234,7 +1235,7 @@ def test_create_streaming_response_content_reasoning() -> None:
|
||||
mock_item_reasoning.summary = ["Problem analysis summary"]
|
||||
mock_event_reasoning.item = mock_item_reasoning
|
||||
|
||||
result = client._create_streaming_response_content(mock_event_reasoning, chat_options, function_call_ids) # type: ignore
|
||||
result = client._parse_chunk_from_openai(mock_event_reasoning, chat_options, function_call_ids) # type: ignore
|
||||
assert len(result.contents) == 1
|
||||
assert isinstance(result.contents[0], TextReasoningContent)
|
||||
assert result.contents[0].text == "Analyzing the problem step by step..."
|
||||
@@ -1242,8 +1243,8 @@ def test_create_streaming_response_content_reasoning() -> None:
|
||||
assert result.contents[0].additional_properties["summary"] == "Problem analysis summary"
|
||||
|
||||
|
||||
def test_openai_content_parser_text_reasoning_comprehensive() -> None:
|
||||
"""Test _openai_content_parser with TextReasoningContent all additional properties."""
|
||||
def test_prepare_content_for_openai_text_reasoning_comprehensive() -> None:
|
||||
"""Test _prepare_content_for_openai with TextReasoningContent all additional properties."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
# Test TextReasoningContent with all additional properties
|
||||
@@ -1255,7 +1256,7 @@ def test_openai_content_parser_text_reasoning_comprehensive() -> None:
|
||||
"encrypted_content": "secure_data_456",
|
||||
},
|
||||
)
|
||||
result = client._openai_content_parser(Role.ASSISTANT, comprehensive_reasoning, {}) # type: ignore
|
||||
result = client._prepare_content_for_openai(Role.ASSISTANT, comprehensive_reasoning, {}) # type: ignore
|
||||
assert result["type"] == "reasoning"
|
||||
assert result["summary"]["text"] == "Comprehensive reasoning summary"
|
||||
assert result["status"] == "in_progress"
|
||||
@@ -1280,7 +1281,7 @@ def test_streaming_reasoning_text_delta_event() -> None:
|
||||
)
|
||||
|
||||
with patch.object(client, "_get_metadata_from_response", return_value={}) as mock_metadata:
|
||||
response = client._create_streaming_response_content(event, chat_options, function_call_ids) # type: ignore
|
||||
response = client._parse_chunk_from_openai(event, chat_options, function_call_ids) # type: ignore
|
||||
|
||||
assert len(response.contents) == 1
|
||||
assert isinstance(response.contents[0], TextReasoningContent)
|
||||
@@ -1305,7 +1306,7 @@ def test_streaming_reasoning_text_done_event() -> None:
|
||||
)
|
||||
|
||||
with patch.object(client, "_get_metadata_from_response", return_value={"test": "data"}) as mock_metadata:
|
||||
response = client._create_streaming_response_content(event, chat_options, function_call_ids) # type: ignore
|
||||
response = client._parse_chunk_from_openai(event, chat_options, function_call_ids) # type: ignore
|
||||
|
||||
assert len(response.contents) == 1
|
||||
assert isinstance(response.contents[0], TextReasoningContent)
|
||||
@@ -1331,7 +1332,7 @@ def test_streaming_reasoning_summary_text_delta_event() -> None:
|
||||
)
|
||||
|
||||
with patch.object(client, "_get_metadata_from_response", return_value={}) as mock_metadata:
|
||||
response = client._create_streaming_response_content(event, chat_options, function_call_ids) # type: ignore
|
||||
response = client._parse_chunk_from_openai(event, chat_options, function_call_ids) # type: ignore
|
||||
|
||||
assert len(response.contents) == 1
|
||||
assert isinstance(response.contents[0], TextReasoningContent)
|
||||
@@ -1356,7 +1357,7 @@ def test_streaming_reasoning_summary_text_done_event() -> None:
|
||||
)
|
||||
|
||||
with patch.object(client, "_get_metadata_from_response", return_value={"custom": "meta"}) as mock_metadata:
|
||||
response = client._create_streaming_response_content(event, chat_options, function_call_ids) # type: ignore
|
||||
response = client._parse_chunk_from_openai(event, chat_options, function_call_ids) # type: ignore
|
||||
|
||||
assert len(response.contents) == 1
|
||||
assert isinstance(response.contents[0], TextReasoningContent)
|
||||
@@ -1392,8 +1393,8 @@ def test_streaming_reasoning_events_preserve_metadata() -> None:
|
||||
)
|
||||
|
||||
with patch.object(client, "_get_metadata_from_response", return_value={"test": "metadata"}):
|
||||
text_response = client._create_streaming_response_content(text_event, chat_options, function_call_ids) # type: ignore
|
||||
reasoning_response = client._create_streaming_response_content(reasoning_event, chat_options, function_call_ids) # type: ignore
|
||||
text_response = client._parse_chunk_from_openai(text_event, chat_options, function_call_ids) # type: ignore
|
||||
reasoning_response = client._parse_chunk_from_openai(reasoning_event, chat_options, function_call_ids) # type: ignore
|
||||
|
||||
# Both should preserve metadata
|
||||
assert text_response.additional_properties == {"test": "metadata"}
|
||||
@@ -1404,7 +1405,7 @@ def test_streaming_reasoning_events_preserve_metadata() -> None:
|
||||
assert isinstance(reasoning_response.contents[0], TextReasoningContent)
|
||||
|
||||
|
||||
def test_create_response_content_image_generation_raw_base64():
|
||||
def test_parse_response_from_openai_image_generation_raw_base64():
|
||||
"""Test image generation response parsing with raw base64 string."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
@@ -1428,7 +1429,7 @@ def test_create_response_content_image_generation_raw_base64():
|
||||
mock_response.output = [mock_item]
|
||||
|
||||
with patch.object(client, "_get_metadata_from_response", return_value={}):
|
||||
response = client._create_response_content(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
response = client._parse_response_from_openai(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
|
||||
# Verify the response contains DataContent with proper URI and media_type
|
||||
assert len(response.messages[0].contents) == 1
|
||||
@@ -1438,7 +1439,7 @@ def test_create_response_content_image_generation_raw_base64():
|
||||
assert content.media_type == "image/png"
|
||||
|
||||
|
||||
def test_create_response_content_image_generation_existing_data_uri():
|
||||
def test_parse_response_from_openai_image_generation_existing_data_uri():
|
||||
"""Test image generation response parsing with existing data URI."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
@@ -1461,7 +1462,7 @@ def test_create_response_content_image_generation_existing_data_uri():
|
||||
mock_response.output = [mock_item]
|
||||
|
||||
with patch.object(client, "_get_metadata_from_response", return_value={}):
|
||||
response = client._create_response_content(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
response = client._parse_response_from_openai(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
|
||||
# Verify the response contains DataContent with proper media_type parsed from URI
|
||||
assert len(response.messages[0].contents) == 1
|
||||
@@ -1471,7 +1472,7 @@ def test_create_response_content_image_generation_existing_data_uri():
|
||||
assert content.media_type == "image/webp"
|
||||
|
||||
|
||||
def test_create_response_content_image_generation_format_detection():
|
||||
def test_parse_response_from_openai_image_generation_format_detection():
|
||||
"""Test different image format detection from base64 data."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
@@ -1493,7 +1494,7 @@ def test_create_response_content_image_generation_format_detection():
|
||||
mock_response_jpeg.output = [mock_item_jpeg]
|
||||
|
||||
with patch.object(client, "_get_metadata_from_response", return_value={}):
|
||||
response_jpeg = client._create_response_content(mock_response_jpeg, chat_options=ChatOptions()) # type: ignore
|
||||
response_jpeg = client._parse_response_from_openai(mock_response_jpeg, chat_options=ChatOptions()) # type: ignore
|
||||
content_jpeg = response_jpeg.messages[0].contents[0]
|
||||
assert isinstance(content_jpeg, DataContent)
|
||||
assert content_jpeg.media_type == "image/jpeg"
|
||||
@@ -1517,14 +1518,14 @@ def test_create_response_content_image_generation_format_detection():
|
||||
mock_response_webp.output = [mock_item_webp]
|
||||
|
||||
with patch.object(client, "_get_metadata_from_response", return_value={}):
|
||||
response_webp = client._create_response_content(mock_response_webp, chat_options=ChatOptions()) # type: ignore
|
||||
response_webp = client._parse_response_from_openai(mock_response_webp, chat_options=ChatOptions()) # type: ignore
|
||||
content_webp = response_webp.messages[0].contents[0]
|
||||
assert isinstance(content_webp, DataContent)
|
||||
assert content_webp.media_type == "image/webp"
|
||||
assert "data:image/webp;base64," in content_webp.uri
|
||||
|
||||
|
||||
def test_create_response_content_image_generation_fallback():
|
||||
def test_parse_response_from_openai_image_generation_fallback():
|
||||
"""Test image generation with invalid base64 falls back to PNG."""
|
||||
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
|
||||
|
||||
@@ -1547,7 +1548,7 @@ def test_create_response_content_image_generation_fallback():
|
||||
mock_response.output = [mock_item]
|
||||
|
||||
with patch.object(client, "_get_metadata_from_response", return_value={}):
|
||||
response = client._create_response_content(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
response = client._parse_response_from_openai(mock_response, chat_options=ChatOptions()) # type: ignore
|
||||
|
||||
# Verify it falls back to PNG format for unrecognized binary data
|
||||
assert len(response.messages[0].contents) == 1
|
||||
@@ -1563,21 +1564,21 @@ async def test_prepare_options_store_parameter_handling() -> None:
|
||||
|
||||
test_conversation_id = "test-conversation-123"
|
||||
chat_options = ChatOptions(store=True, conversation_id=test_conversation_id)
|
||||
options = await client.prepare_options(messages, chat_options)
|
||||
options = await client._prepare_options(messages, chat_options) # type: ignore
|
||||
assert options["store"] is True
|
||||
assert options["previous_response_id"] == test_conversation_id
|
||||
|
||||
chat_options = ChatOptions(store=False, conversation_id="")
|
||||
options = await client.prepare_options(messages, chat_options)
|
||||
options = await client._prepare_options(messages, chat_options) # type: ignore
|
||||
assert options["store"] is False
|
||||
|
||||
chat_options = ChatOptions(store=None, conversation_id=None)
|
||||
options = await client.prepare_options(messages, chat_options)
|
||||
options = await client._prepare_options(messages, chat_options) # type: ignore
|
||||
assert "store" not in options
|
||||
assert "previous_response_id" not in options
|
||||
|
||||
chat_options = ChatOptions()
|
||||
options = await client.prepare_options(messages, chat_options)
|
||||
options = await client._prepare_options(messages, chat_options) # type: ignore
|
||||
assert "store" not in options
|
||||
assert "previous_response_id" not in options
|
||||
|
||||
|
||||
Reference in New Issue
Block a user