Python: Added tests for OpenAI content types + Unit test improvement (#3259)

* added tests for content types+ unit test improvement

* small fixes

* small fix
This commit is contained in:
Giles Odigwe
2026-01-22 22:04:36 -08:00
committed by GitHub
Unverified
parent 50c2539f3a
commit e229dfa7e5
3 changed files with 948 additions and 0 deletions
@@ -609,6 +609,81 @@ def test_parse_function_calls_from_assistants_basic(mock_async_openai: MagicMock
assert contents[0].arguments == {"location": "Seattle"}
def test_parse_run_step_with_code_interpreter_tool_call(mock_async_openai: MagicMock) -> None:
"""Test _parse_run_step_tool_call with code_interpreter type creates CodeInterpreterToolCallContent."""
client = create_test_openai_assistants_client(
mock_async_openai,
model_id="test-model",
assistant_id="test-assistant",
)
# Mock a run with required_action containing code_interpreter tool call
mock_run = MagicMock()
mock_run.id = "run_123"
mock_run.status = "requires_action"
mock_tool_call = MagicMock()
mock_tool_call.id = "call_code_123"
mock_tool_call.type = "code_interpreter"
mock_code_interpreter = MagicMock()
mock_code_interpreter.input = "print('Hello, World!')"
mock_tool_call.code_interpreter = mock_code_interpreter
mock_required_action = MagicMock()
mock_required_action.submit_tool_outputs = MagicMock()
mock_required_action.submit_tool_outputs.tool_calls = [mock_tool_call]
mock_run.required_action = mock_required_action
# Parse the run step
contents = client._parse_function_calls_from_assistants(mock_run, "response_123")
# Should have CodeInterpreterToolCallContent
assert len(contents) == 1
assert contents[0].type == "code_interpreter_tool_call"
assert contents[0].call_id == '["response_123", "call_code_123"]'
assert contents[0].inputs is not None
assert len(contents[0].inputs) == 1
assert contents[0].inputs[0].type == "text"
assert contents[0].inputs[0].text == "print('Hello, World!')"
def test_parse_run_step_with_mcp_tool_call(mock_async_openai: MagicMock) -> None:
"""Test _parse_run_step_tool_call with mcp type creates MCPServerToolCallContent."""
client = create_test_openai_assistants_client(
mock_async_openai,
model_id="test-model",
assistant_id="test-assistant",
)
# Mock a run with required_action containing mcp tool call
mock_run = MagicMock()
mock_run.id = "run_456"
mock_run.status = "requires_action"
mock_tool_call = MagicMock()
mock_tool_call.id = "call_mcp_456"
mock_tool_call.type = "mcp"
mock_tool_call.name = "fetch_data"
mock_tool_call.server_label = "DataServer"
mock_tool_call.args = {"key": "value"}
mock_required_action = MagicMock()
mock_required_action.submit_tool_outputs = MagicMock()
mock_required_action.submit_tool_outputs.tool_calls = [mock_tool_call]
mock_run.required_action = mock_required_action
# Parse the run step
contents = client._parse_function_calls_from_assistants(mock_run, "response_456")
# Should have MCPServerToolCallContent
assert len(contents) == 1
assert contents[0].type == "mcp_server_tool_call"
assert contents[0].call_id == '["response_456", "call_mcp_456"]'
assert contents[0].tool_name == "fetch_data"
assert contents[0].server_name == "DataServer"
assert contents[0].arguments == {"key": "value"}
def test_prepare_options_basic(mock_async_openai: MagicMock) -> None:
"""Test _prepare_options with basic chat options."""
chat_client = create_test_openai_assistants_client(mock_async_openai)
@@ -7,6 +7,8 @@ from unittest.mock import MagicMock, patch
import pytest
from openai import BadRequestError
from openai.types.chat.chat_completion import ChatCompletion, Choice
from openai.types.chat.chat_completion_message import ChatCompletionMessage
from pydantic import BaseModel
from pytest import param
@@ -497,6 +499,430 @@ def test_prepare_content_for_openai_document_file_mapping(openai_unit_test_env:
assert "filename" not in result["file"] # None filename should be omitted
def test_parse_text_reasoning_content_from_response(openai_unit_test_env: dict[str, str]) -> None:
"""Test that TextReasoningContent is correctly parsed from OpenAI response with reasoning_details."""
client = OpenAIChatClient()
# Mock response with reasoning_details
mock_reasoning_details = {
"effort": "high",
"summary": "Analyzed the problem carefully",
"content": [{"type": "reasoning_text", "text": "Step-by-step thinking..."}],
}
mock_response = ChatCompletion(
id="test-response",
object="chat.completion",
created=1234567890,
model="gpt-5",
choices=[
Choice(
index=0,
message=ChatCompletionMessage(
role="assistant",
content="The answer is 42.",
reasoning_details=mock_reasoning_details,
),
finish_reason="stop",
)
],
)
response = client._parse_response_from_openai(mock_response, {})
# Should have both text and reasoning content
assert len(response.messages) == 1
message = response.messages[0]
assert len(message.contents) == 2
# First should be text content
assert message.contents[0].type == "text"
assert message.contents[0].text == "The answer is 42."
# Second should be reasoning content with protected_data
assert message.contents[1].type == "text_reasoning"
assert message.contents[1].protected_data is not None
parsed_details = json.loads(message.contents[1].protected_data)
assert parsed_details == mock_reasoning_details
def test_parse_text_reasoning_content_from_streaming_chunk(openai_unit_test_env: dict[str, str]) -> None:
"""Test that TextReasoningContent is correctly parsed from streaming OpenAI chunk with reasoning_details."""
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_chunk import ChoiceDelta as ChunkChoiceDelta
client = OpenAIChatClient()
# Mock streaming chunk with reasoning_details
mock_reasoning_details = {
"type": "reasoning",
"content": "Analyzing the question...",
}
mock_chunk = ChatCompletionChunk(
id="test-chunk",
object="chat.completion.chunk",
created=1234567890,
model="gpt-5",
choices=[
ChunkChoice(
index=0,
delta=ChunkChoiceDelta(
role="assistant",
content="Partial answer",
reasoning_details=mock_reasoning_details,
),
finish_reason=None,
)
],
)
update = client._parse_response_update_from_openai(mock_chunk)
# Should have both text and reasoning content
assert len(update.contents) == 2
# First should be text content
assert update.contents[0].type == "text"
assert update.contents[0].text == "Partial answer"
# Second should be reasoning content
assert update.contents[1].type == "text_reasoning"
assert update.contents[1].protected_data is not None
parsed_details = json.loads(update.contents[1].protected_data)
assert parsed_details == mock_reasoning_details
def test_prepare_message_with_text_reasoning_content(openai_unit_test_env: dict[str, str]) -> None:
"""Test that TextReasoningContent with protected_data is correctly prepared for OpenAI."""
client = OpenAIChatClient()
# Create message with text_reasoning content that has protected_data
# text_reasoning is meant to be added to an existing message, so include text content first
mock_reasoning_data = {
"effort": "medium",
"summary": "Quick analysis",
}
reasoning_content = Content.from_text_reasoning(text=None, protected_data=json.dumps(mock_reasoning_data))
# Message must have other content first for reasoning to attach to
message = ChatMessage(
role="assistant",
contents=[
Content.from_text(text="The answer is 42."),
reasoning_content,
],
)
prepared = client._prepare_message_for_openai(message)
# Should have one message with reasoning_details attached
assert len(prepared) == 1
assert "reasoning_details" in prepared[0]
assert prepared[0]["reasoning_details"] == mock_reasoning_data
# Should also have the text content
assert prepared[0]["content"][0]["type"] == "text"
assert prepared[0]["content"][0]["text"] == "The answer is 42."
def test_function_approval_content_is_skipped_in_preparation(openai_unit_test_env: dict[str, str]) -> None:
"""Test that function approval request and response content are skipped."""
client = OpenAIChatClient()
# Create approval request
function_call = Content.from_function_call(
call_id="call_123",
name="dangerous_action",
arguments='{"confirm": true}',
)
approval_request = Content.from_function_approval_request(
id="approval_001",
function_call=function_call,
)
# Create approval response
approval_response = Content.from_function_approval_response(
approved=False,
id="approval_001",
function_call=function_call,
)
# Test that approval request is skipped
message_with_request = ChatMessage(role="assistant", contents=[approval_request])
prepared_request = client._prepare_message_for_openai(message_with_request)
assert len(prepared_request) == 0 # Should be empty - approval content is skipped
# Test that approval response is skipped
message_with_response = ChatMessage(role="user", contents=[approval_response])
prepared_response = client._prepare_message_for_openai(message_with_response)
assert len(prepared_response) == 0 # Should be empty - approval content is skipped
# Test with mixed content - approval should be skipped, text should remain
mixed_message = ChatMessage(
role="assistant",
contents=[
Content.from_text(text="I need approval for this action."),
approval_request,
],
)
prepared_mixed = client._prepare_message_for_openai(mixed_message)
assert len(prepared_mixed) == 1 # Only text content should remain
assert prepared_mixed[0]["content"][0]["type"] == "text"
assert prepared_mixed[0]["content"][0]["text"] == "I need approval for this action."
def test_usage_content_in_streaming_response(openai_unit_test_env: dict[str, str]) -> None:
"""Test that UsageContent is correctly parsed from streaming response with usage data."""
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
from openai.types.completion_usage import CompletionUsage
client = OpenAIChatClient()
# Mock streaming chunk with usage data (typically last chunk)
mock_usage = CompletionUsage(
prompt_tokens=100,
completion_tokens=50,
total_tokens=150,
)
mock_chunk = ChatCompletionChunk(
id="test-chunk",
object="chat.completion.chunk",
created=1234567890,
model="gpt-4o",
choices=[], # Empty choices when sending usage
usage=mock_usage,
)
update = client._parse_response_update_from_openai(mock_chunk)
# Should have usage content
assert len(update.contents) == 1
assert update.contents[0].type == "usage"
usage_content = update.contents[0]
assert isinstance(usage_content.usage_details, dict)
assert usage_content.usage_details["input_token_count"] == 100
assert usage_content.usage_details["output_token_count"] == 50
assert usage_content.usage_details["total_token_count"] == 150
def test_parse_text_with_refusal(openai_unit_test_env: dict[str, str]) -> None:
"""Test that refusal content is parsed correctly."""
from openai.types.chat.chat_completion import ChatCompletion, Choice
from openai.types.chat.chat_completion_message import ChatCompletionMessage
client = OpenAIChatClient()
# Mock response with refusal
mock_response = ChatCompletion(
id="test-response",
object="chat.completion",
created=1234567890,
model="gpt-4o",
choices=[
Choice(
index=0,
message=ChatCompletionMessage(
role="assistant",
content=None,
refusal="I cannot provide that information.",
),
finish_reason="stop",
)
],
)
response = client._parse_response_from_openai(mock_response, {})
# Should have text content with refusal message
assert len(response.messages) == 1
message = response.messages[0]
assert len(message.contents) == 1
assert message.contents[0].type == "text"
assert message.contents[0].text == "I cannot provide that information."
def test_prepare_options_without_model_id(openai_unit_test_env: dict[str, str]) -> None:
"""Test that prepare_options raises error when model_id is not set."""
client = OpenAIChatClient()
client.model_id = None # Remove model_id
messages = [ChatMessage(role="user", text="test")]
with pytest.raises(ValueError, match="model_id must be a non-empty string"):
client._prepare_options(messages, {})
def test_prepare_options_without_messages(openai_unit_test_env: dict[str, str]) -> None:
"""Test that prepare_options raises error when messages are missing."""
from agent_framework.exceptions import ServiceInvalidRequestError
client = OpenAIChatClient()
with pytest.raises(ServiceInvalidRequestError, match="Messages are required"):
client._prepare_options([], {})
def test_prepare_tools_with_web_search_no_location(openai_unit_test_env: dict[str, str]) -> None:
"""Test preparing web search tool without user location."""
client = OpenAIChatClient()
# Web search tool without additional_properties
web_search_tool = HostedWebSearchTool()
result = client._prepare_tools_for_openai([web_search_tool])
# Should have empty web_search_options (no location)
assert "web_search_options" in result
assert result["web_search_options"] == {}
def test_prepare_options_with_instructions(openai_unit_test_env: dict[str, str]) -> None:
"""Test that instructions are prepended as system message."""
client = OpenAIChatClient()
messages = [ChatMessage(role="user", text="Hello")]
options = {"instructions": "You are a helpful assistant."}
prepared_options = client._prepare_options(messages, options)
# Should have messages with system message prepended
assert "messages" in prepared_options
assert len(prepared_options["messages"]) == 2
assert prepared_options["messages"][0]["role"] == "system"
assert prepared_options["messages"][0]["content"][0]["text"] == "You are a helpful assistant."
def test_prepare_message_with_author_name(openai_unit_test_env: dict[str, str]) -> None:
"""Test that author_name is included in prepared message."""
client = OpenAIChatClient()
message = ChatMessage(
role="user",
author_name="TestUser",
contents=[Content.from_text(text="Hello")],
)
prepared = client._prepare_message_for_openai(message)
assert len(prepared) == 1
assert prepared[0]["name"] == "TestUser"
def test_prepare_message_with_tool_result_author_name(openai_unit_test_env: dict[str, str]) -> None:
"""Test that author_name is not included for TOOL role messages."""
client = OpenAIChatClient()
# Tool messages should not have 'name' field (it's for function name instead)
message = ChatMessage(
role="tool",
author_name="ShouldNotAppear",
contents=[Content.from_function_result(call_id="call_123", result="result")],
)
prepared = client._prepare_message_for_openai(message)
assert len(prepared) == 1
# Should not have 'name' field for tool messages
assert "name" not in prepared[0]
def test_tool_choice_required_with_function_name(openai_unit_test_env: dict[str, str]) -> None:
"""Test that tool_choice with required mode and function name is correctly prepared."""
client = OpenAIChatClient()
messages = [ChatMessage(role="user", text="test")]
options = {
"tools": [get_weather],
"tool_choice": {"mode": "required", "required_function_name": "get_weather"},
}
prepared_options = client._prepare_options(messages, options)
# Should format tool_choice correctly
assert "tool_choice" in prepared_options
assert prepared_options["tool_choice"]["type"] == "function"
assert prepared_options["tool_choice"]["function"]["name"] == "get_weather"
def test_response_format_dict_passthrough(openai_unit_test_env: dict[str, str]) -> None:
"""Test that response_format as dict is passed through directly."""
client = OpenAIChatClient()
messages = [ChatMessage(role="user", text="test")]
custom_format = {
"type": "json_schema",
"json_schema": {"name": "Test", "schema": {"type": "object"}},
}
options = {"response_format": custom_format}
prepared_options = client._prepare_options(messages, options)
# Should pass through the dict directly
assert prepared_options["response_format"] == custom_format
def test_multiple_function_calls_in_single_message(openai_unit_test_env: dict[str, str]) -> None:
"""Test that multiple function calls in a message are correctly prepared."""
client = OpenAIChatClient()
# Create message with multiple function calls
message = ChatMessage(
role="assistant",
contents=[
Content.from_function_call(call_id="call_1", name="func_1", arguments='{"a": 1}'),
Content.from_function_call(call_id="call_2", name="func_2", arguments='{"b": 2}'),
],
)
prepared = client._prepare_message_for_openai(message)
# Should have one message with multiple tool_calls
assert len(prepared) == 1
assert "tool_calls" in prepared[0]
assert len(prepared[0]["tool_calls"]) == 2
assert prepared[0]["tool_calls"][0]["id"] == "call_1"
assert prepared[0]["tool_calls"][1]["id"] == "call_2"
def test_prepare_options_removes_parallel_tool_calls_when_no_tools(openai_unit_test_env: dict[str, str]) -> None:
"""Test that parallel_tool_calls is removed when no tools are present."""
client = OpenAIChatClient()
messages = [ChatMessage(role="user", text="test")]
options = {"allow_multiple_tool_calls": True}
prepared_options = client._prepare_options(messages, options)
# Should not have parallel_tool_calls when no tools
assert "parallel_tool_calls" not in prepared_options
async def test_streaming_exception_handling(openai_unit_test_env: dict[str, str]) -> None:
"""Test that streaming errors are properly handled."""
client = OpenAIChatClient()
messages = [ChatMessage(role="user", text="test")]
# Create a mock error during streaming
mock_error = Exception("Streaming error")
with (
patch.object(client.client.chat.completions, "create", side_effect=mock_error),
pytest.raises(ServiceResponseException),
):
async def consume_stream():
async for _ in client._inner_get_streaming_response(messages=messages, options={}): # type: ignore
pass
await consume_stream()
# region Integration Tests
@@ -642,6 +642,453 @@ def test_response_content_creation_with_function_call() -> None:
assert function_call.arguments == '{"location": "Seattle"}'
def test_prepare_content_for_openai_function_approval_response() -> None:
"""Test _prepare_content_for_openai with function approval response content."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Test approved response
function_call = Content.from_function_call(
call_id="call_123",
name="send_email",
arguments='{"to": "user@example.com"}',
)
approval_response = Content.from_function_approval_response(
approved=True,
id="approval_001",
function_call=function_call,
)
result = client._prepare_content_for_openai(Role.ASSISTANT, approval_response, {})
assert result["type"] == "mcp_approval_response"
assert result["approval_request_id"] == "approval_001"
assert result["approve"] is True
def test_prepare_content_for_openai_error_content() -> None:
"""Test _prepare_content_for_openai with error content."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
error_content = Content.from_error(
message="Operation failed",
error_code="ERR_123",
error_details="Invalid parameter",
)
result = client._prepare_content_for_openai(Role.ASSISTANT, error_content, {})
# ErrorContent should return empty dict (logged but not sent)
assert result == {}
def test_prepare_content_for_openai_usage_content() -> None:
"""Test _prepare_content_for_openai with usage content."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
usage_content = Content.from_usage(
usage_details={
"input_token_count": 100,
"output_token_count": 50,
"total_token_count": 150,
}
)
result = client._prepare_content_for_openai(Role.ASSISTANT, usage_content, {})
# UsageContent should return empty dict (logged but not sent)
assert result == {}
def test_prepare_content_for_openai_hosted_vector_store_content() -> None:
"""Test _prepare_content_for_openai with hosted vector store content."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
vector_store_content = Content.from_hosted_vector_store(
vector_store_id="vs_123",
)
result = client._prepare_content_for_openai(Role.ASSISTANT, vector_store_content, {})
# HostedVectorStoreContent should return empty dict (logged but not sent)
assert result == {}
def test_parse_response_from_openai_with_mcp_server_tool_result() -> None:
"""Test _parse_response_from_openai with MCP server tool result."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
mock_response = MagicMock()
mock_response.output_parsed = None
mock_response.metadata = {}
mock_response.usage = None
mock_response.id = "resp-id"
mock_response.model = "test-model"
mock_response.created_at = 1000000000
# Mock MCP call item with result
mock_mcp_item = MagicMock()
mock_mcp_item.type = "mcp_call"
mock_mcp_item.id = "mcp_call_123"
mock_mcp_item.name = "get_data"
mock_mcp_item.arguments = {"key": "value"}
mock_mcp_item.server_label = "TestServer"
mock_mcp_item.result = [{"content": [{"type": "text", "text": "MCP result"}]}]
mock_response.output = [mock_mcp_item]
response = client._parse_response_from_openai(mock_response, options={}) # type: ignore
# Should have both call and result content
assert len(response.messages[0].contents) == 2
call_content, result_content = response.messages[0].contents
assert call_content.type == "mcp_server_tool_call"
assert call_content.call_id == "mcp_call_123"
assert call_content.tool_name == "get_data"
assert call_content.server_name == "TestServer"
assert result_content.type == "mcp_server_tool_result"
assert result_content.call_id == "mcp_call_123"
assert result_content.output is not None
def test_parse_chunk_from_openai_with_mcp_call_result() -> None:
"""Test _parse_chunk_from_openai with MCP call output."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Mock event with MCP call that has output
mock_event = MagicMock()
mock_event.type = "response.output_item.added"
mock_item = MagicMock()
mock_item.type = "mcp_call"
mock_item.id = "mcp_call_456"
mock_item.call_id = "call_456"
mock_item.name = "fetch_resource"
mock_item.server_label = "ResourceServer"
mock_item.arguments = {"resource_id": "123"}
# Use proper content structure that _parse_content can handle
mock_item.result = [{"type": "text", "text": "test result"}]
mock_event.item = mock_item
mock_event.output_index = 0
function_call_ids: dict[int, tuple[str, str]] = {}
update = client._parse_chunk_from_openai(mock_event, options={}, function_call_ids=function_call_ids)
# Should have both call and result in contents
assert len(update.contents) == 2
call_content, result_content = update.contents
assert call_content.type == "mcp_server_tool_call"
assert call_content.call_id in ["mcp_call_456", "call_456"]
assert call_content.tool_name == "fetch_resource"
assert result_content.type == "mcp_server_tool_result"
assert result_content.call_id in ["mcp_call_456", "call_456"]
# Verify the output was parsed
assert result_content.output is not None
def test_prepare_message_for_openai_with_function_approval_response() -> None:
"""Test _prepare_message_for_openai with function approval response content in messages."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
function_call = Content.from_function_call(
call_id="call_789",
name="execute_command",
arguments='{"command": "ls"}',
)
approval_response = Content.from_function_approval_response(
approved=True,
id="approval_003",
function_call=function_call,
)
message = ChatMessage(role="user", contents=[approval_response])
call_id_to_id: dict[str, str] = {}
result = client._prepare_message_for_openai(message, call_id_to_id)
# FunctionApprovalResponseContent is added directly, not nested in args with role
assert len(result) == 1
prepared_message = result[0]
assert prepared_message["type"] == "mcp_approval_response"
assert prepared_message["approval_request_id"] == "approval_003"
assert prepared_message["approve"] is True
def test_chat_message_with_error_content() -> None:
"""Test that error content in messages is handled properly."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
error_content = Content.from_error(
message="Test error",
error_code="TEST_ERR",
)
message = ChatMessage(role="assistant", contents=[error_content])
call_id_to_id: dict[str, str] = {}
result = client._prepare_message_for_openai(message, call_id_to_id)
# Message should be prepared with empty content list since ErrorContent returns {}
assert len(result) == 1
prepared_message = result[0]
assert prepared_message["role"] == "assistant"
# Content should be a list with empty dict since ErrorContent returns {}
assert prepared_message.get("content") == [{}]
def test_chat_message_with_usage_content() -> None:
"""Test that usage content in messages is handled properly."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
usage_content = Content.from_usage(
usage_details={
"input_token_count": 200,
"output_token_count": 100,
"total_token_count": 300,
}
)
message = ChatMessage(role="assistant", contents=[usage_content])
call_id_to_id: dict[str, str] = {}
result = client._prepare_message_for_openai(message, call_id_to_id)
# Message should be prepared with empty content list since UsageContent returns {}
assert len(result) == 1
prepared_message = result[0]
assert prepared_message["role"] == "assistant"
# Content should be a list with empty dict since UsageContent returns {}
assert prepared_message.get("content") == [{}]
def test_hosted_file_content_preparation() -> None:
"""Test _prepare_content_for_openai with hosted file content."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
hosted_file = Content.from_hosted_file(
file_id="file_abc123",
media_type="application/pdf",
name="document.pdf",
)
result = client._prepare_content_for_openai(Role.USER, hosted_file, {})
assert result["type"] == "input_file"
assert result["file_id"] == "file_abc123"
def test_function_approval_response_with_mcp_tool_call() -> None:
"""Test function approval response content with MCP server tool call content."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
mcp_call = Content.from_mcp_server_tool_call(
call_id="mcp_call_999",
tool_name="sensitive_action",
server_name="SecureServer",
arguments={"action": "delete"},
)
approval_response = Content.from_function_approval_response(
approved=False,
id="approval_mcp_001",
function_call=mcp_call,
)
result = client._prepare_content_for_openai(Role.ASSISTANT, approval_response, {})
assert result["type"] == "mcp_approval_response"
assert result["approval_request_id"] == "approval_mcp_001"
assert result["approve"] is False
def test_response_format_with_conflicting_definitions() -> None:
"""Test that conflicting response_format definitions raise an error."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
# Mock response_format and text_config that conflict
response_format = {"type": "json_schema", "format": {"type": "json_schema", "name": "Test", "schema": {}}}
text_config = {"format": {"type": "json_object"}}
with pytest.raises(ServiceInvalidRequestError, match="Conflicting response_format definitions"):
client._prepare_response_and_text_format(response_format=response_format, text_config=text_config)
def test_response_format_json_object_type() -> None:
"""Test response_format with json_object type."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
response_format = {"type": "json_object"}
_, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None)
assert text_config is not None
assert text_config["format"]["type"] == "json_object"
def test_response_format_text_type() -> None:
"""Test response_format with text type."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
response_format = {"type": "text"}
_, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None)
assert text_config is not None
assert text_config["format"]["type"] == "text"
def test_response_format_with_format_key() -> None:
"""Test response_format that already has a format key."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
response_format = {"format": {"type": "json_schema", "name": "MySchema", "schema": {"type": "object"}}}
_, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None)
assert text_config is not None
assert text_config["format"]["type"] == "json_schema"
assert text_config["format"]["name"] == "MySchema"
def test_response_format_json_schema_no_name_uses_title() -> None:
"""Test json_schema response_format without name uses title from schema."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
response_format = {
"type": "json_schema",
"json_schema": {"schema": {"title": "MyTitle", "type": "object", "properties": {}}},
}
_, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None)
assert text_config is not None
assert text_config["format"]["name"] == "MyTitle"
def test_response_format_json_schema_with_strict() -> None:
"""Test json_schema response_format with strict mode."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
response_format = {
"type": "json_schema",
"json_schema": {"name": "StrictSchema", "schema": {"type": "object"}, "strict": True},
}
_, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None)
assert text_config is not None
assert text_config["format"]["strict"] is True
def test_response_format_json_schema_with_description() -> None:
"""Test json_schema response_format with description."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
response_format = {
"type": "json_schema",
"json_schema": {
"name": "DescribedSchema",
"schema": {"type": "object"},
"description": "A test schema",
},
}
_, text_config = client._prepare_response_and_text_format(response_format=response_format, text_config=None)
assert text_config is not None
assert text_config["format"]["description"] == "A test schema"
def test_response_format_json_schema_missing_schema() -> None:
"""Test json_schema response_format without schema raises error."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
response_format = {"type": "json_schema", "json_schema": {"name": "NoSchema"}}
with pytest.raises(ServiceInvalidRequestError, match="json_schema response_format requires a schema"):
client._prepare_response_and_text_format(response_format=response_format, text_config=None)
def test_response_format_unsupported_type() -> None:
"""Test unsupported response_format type raises error."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
response_format = {"type": "unsupported_format"}
with pytest.raises(ServiceInvalidRequestError, match="Unsupported response_format"):
client._prepare_response_and_text_format(response_format=response_format, text_config=None)
def test_response_format_invalid_type() -> None:
"""Test invalid response_format type raises error."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
response_format = "invalid" # Not a Pydantic model or mapping
with pytest.raises(ServiceInvalidRequestError, match="response_format must be a Pydantic model or mapping"):
client._prepare_response_and_text_format(response_format=response_format, text_config=None) # type: ignore
def test_parse_response_with_store_false() -> None:
"""Test _get_conversation_id returns None when store is False."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
mock_response = MagicMock()
mock_response.id = "resp_123"
mock_response.conversation = MagicMock()
mock_response.conversation.id = "conv_456"
conversation_id = client._get_conversation_id(mock_response, store=False)
assert conversation_id is None
def test_parse_response_uses_response_id_when_no_conversation() -> None:
"""Test _get_conversation_id returns response ID when no conversation exists."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
mock_response = MagicMock()
mock_response.id = "resp_789"
mock_response.conversation = None
conversation_id = client._get_conversation_id(mock_response, store=True)
assert conversation_id == "resp_789"
def test_streaming_chunk_with_usage_only() -> None:
"""Test streaming chunk that only contains usage info."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
chat_options = ChatOptions()
function_call_ids: dict[int, tuple[str, str]] = {}
mock_event = MagicMock()
mock_event.type = "response.completed"
mock_event.response = MagicMock()
mock_event.response.id = "resp_usage"
mock_event.response.model = "test-model"
mock_event.response.conversation = None
mock_event.response.usage = MagicMock()
mock_event.response.usage.input_tokens = 50
mock_event.response.usage.output_tokens = 25
mock_event.response.usage.total_tokens = 75
mock_event.response.usage.input_tokens_details = None
mock_event.response.usage.output_tokens_details = None
update = client._parse_chunk_from_openai(mock_event, chat_options, function_call_ids)
# Should have usage content
assert len(update.contents) == 1
assert update.contents[0].type == "usage"
assert update.contents[0].usage_details["total_token_count"] == 75
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")