Python: [BREAKING] Types API Review improvements (#3647)

* Replace Role and FinishReason classes with NewType + Literal

- Remove EnumLike metaclass from _types.py
- Replace Role class with NewType('Role', str) + RoleLiteral
- Replace FinishReason class with NewType('FinishReason', str) + FinishReasonLiteral
- Update all usages across codebase to use string literals
- Remove .value access patterns (direct string comparison now works)
- Add backward compatibility for legacy dict serialization format
- Update tests to reflect new string-based types

Addresses #3591, #3615

* Simplify ChatResponse and AgentResponse type hints (#3592)

- Remove overloads from ChatResponse.__init__
- Remove text parameter from ChatResponse.__init__
- Remove | dict[str, Any] from finish_reason and usage_details params
- Remove **kwargs from AgentResponse.__init__
- Both now accept ChatMessage | Sequence[ChatMessage] | None for messages
- Update docstrings and examples to reflect changes
- Fix tests that were using removed kwargs
- Fix Role type hint usage in ag-ui utils

* Remove text parameter from ChatResponseUpdate and AgentResponseUpdate (#3597)

- Remove text parameter from ChatResponseUpdate.__init__
- Remove text parameter from AgentResponseUpdate.__init__
- Remove **kwargs from both update classes
- Simplify contents parameter type to Sequence[Content] | None
- Update all usages to use contents=[Content.from_text(...)] pattern
- Fix imports in test files
- Update docstrings and examples

* Rename from_chat_response_updates to from_updates (#3593)

- ChatResponse.from_chat_response_updates → ChatResponse.from_updates
- ChatResponse.from_chat_response_generator → ChatResponse.from_update_generator
- AgentResponse.from_agent_run_response_updates → AgentResponse.from_updates

* Remove try_parse_value method from ChatResponse and AgentResponse (#3595)

- Remove try_parse_value method from ChatResponse
- Remove try_parse_value method from AgentResponse
- Remove try_parse_value calls from from_updates and from_update_generator methods
- Update samples to use try/except with response.value instead
- Update tests to use response.value pattern
- Users should now use response.value with try/except for safe parsing

* Add agent_id to AgentResponse and clarify author_name documentation (#3596)

- Add agent_id parameter to AgentResponse class
- Document that author_name is on ChatMessage objects, not responses
- Update ChatResponse docstring with author_name note
- Update AgentResponse docstring with author_name note

* Simplify ChatMessage.__init__ signature (#3618)

- Make contents a positional argument accepting Sequence[Content | str]
- Auto-convert strings in contents to TextContent
- Remove overloads, keep text kwarg for backward compatibility with serialization
- Update _parse_content_list to handle string items
- Update all usages across codebase to use new format: ChatMessage("role", ["text"])

* Allow Content as input on run and get_response

- Update prepare_messages and normalize_messages to accept Content
- Update type signatures in _agents.py and _clients.py
- Add tests for Content input handling

* Fix ChatMessage usage across packages and samples

Update all remaining ChatMessage(role=..., text=...) to use new
ChatMessage('role', ['text']) signature.

* Fix Role string usage and response format parsing

- Fix redis provider: remove .value access on string literals
- Fix durabletask ensure_response_format: set _response_format before accessing .value

* Fix ollama .value and ai_model_id issues, handle None in content list

- Fix ollama _chat_client: remove .value on string literals
- Fix ollama _chat_client: rename ai_model_id to model_id
- Fix _parse_content_list: skip None values gracefully

* Fix A2AAgent type signature to include Content

* Fix Role/FinishReason NewType dict annotations and improve test coverage to 95%

* Fix mypy errors for Role/FinishReason NewType usage

* Fix Role.TOOL and Role.ASSISTANT usage in _orchestrator_helpers.py

* Fix Role NewType usage in durabletask _models.py
This commit is contained in:
Eduard van Valkenburg
2026-02-04 10:13:23 +00:00
committed by GitHub
parent ef798629e5
commit 838a7fd61d
341 changed files with 3766 additions and 3228 deletions
@@ -22,7 +22,6 @@ from agent_framework import (
Content,
HostedCodeInterpreterTool,
HostedFileSearchTool,
Role,
tool,
)
from agent_framework.exceptions import ServiceInitializationError
@@ -405,7 +404,7 @@ async def test_process_stream_events_thread_run_created(mock_async_openai: Magic
update = updates[0]
assert isinstance(update, ChatResponseUpdate)
assert update.conversation_id == thread_id
assert update.role == Role.ASSISTANT
assert update.role == "assistant"
assert update.contents == []
assert update.raw_representation == mock_response.data
@@ -449,7 +448,7 @@ async def test_process_stream_events_message_delta_text(mock_async_openai: Magic
update = updates[0]
assert isinstance(update, ChatResponseUpdate)
assert update.conversation_id == thread_id
assert update.role == Role.ASSISTANT
assert update.role == "assistant"
assert update.text == "Hello from assistant"
assert update.raw_representation == mock_message_delta
@@ -488,7 +487,7 @@ async def test_process_stream_events_requires_action(mock_async_openai: MagicMoc
update = updates[0]
assert isinstance(update, ChatResponseUpdate)
assert update.conversation_id == thread_id
assert update.role == Role.ASSISTANT
assert update.role == "assistant"
assert len(update.contents) == 1
assert update.contents[0] == test_function_content
assert update.raw_representation == mock_run
@@ -568,7 +567,7 @@ async def test_process_stream_events_run_completed_with_usage(
update = updates[0]
assert isinstance(update, ChatResponseUpdate)
assert update.conversation_id == thread_id
assert update.role == Role.ASSISTANT
assert update.role == "assistant"
assert len(update.contents) == 1
# Check the usage content
@@ -696,7 +695,7 @@ def test_prepare_options_basic(mock_async_openai: MagicMock) -> None:
"top_p": 0.9,
}
messages = [ChatMessage(role=Role.USER, text="Hello")]
messages = [ChatMessage("user", ["Hello"])]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore
@@ -725,7 +724,7 @@ def test_prepare_options_with_tool_tool(mock_async_openai: MagicMock) -> None:
"tool_choice": "auto",
}
messages = [ChatMessage(role=Role.USER, text="Hello")]
messages = [ChatMessage("user", ["Hello"])]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore
@@ -750,7 +749,7 @@ def test_prepare_options_with_code_interpreter(mock_async_openai: MagicMock) ->
"tool_choice": "auto",
}
messages = [ChatMessage(role=Role.USER, text="Calculate something")]
messages = [ChatMessage("user", ["Calculate something"])]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore
@@ -770,7 +769,7 @@ def test_prepare_options_tool_choice_none(mock_async_openai: MagicMock) -> None:
"tool_choice": "none",
}
messages = [ChatMessage(role=Role.USER, text="Hello")]
messages = [ChatMessage("user", ["Hello"])]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore
@@ -791,7 +790,7 @@ def test_prepare_options_required_function(mock_async_openai: MagicMock) -> None
"tool_choice": tool_choice,
}
messages = [ChatMessage(role=Role.USER, text="Hello")]
messages = [ChatMessage("user", ["Hello"])]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore
@@ -817,7 +816,7 @@ def test_prepare_options_with_file_search_tool(mock_async_openai: MagicMock) ->
"tool_choice": "auto",
}
messages = [ChatMessage(role=Role.USER, text="Search for information")]
messages = [ChatMessage("user", ["Search for information"])]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore
@@ -842,7 +841,7 @@ def test_prepare_options_with_mapping_tool(mock_async_openai: MagicMock) -> None
"tool_choice": "auto",
}
messages = [ChatMessage(role=Role.USER, text="Use custom tool")]
messages = [ChatMessage("user", ["Use custom tool"])]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, options) # type: ignore
@@ -864,7 +863,7 @@ def test_prepare_options_with_pydantic_response_format(mock_async_openai: MagicM
model_config = ConfigDict(extra="forbid")
chat_client = create_test_openai_assistants_client(mock_async_openai)
messages = [ChatMessage(role=Role.USER, text="Test")]
messages = [ChatMessage("user", ["Test"])]
options = {"response_format": TestResponse}
run_options, _ = chat_client._prepare_options(messages, options) # type: ignore
@@ -880,8 +879,8 @@ def test_prepare_options_with_system_message(mock_async_openai: MagicMock) -> No
chat_client = create_test_openai_assistants_client(mock_async_openai)
messages = [
ChatMessage(role=Role.SYSTEM, text="You are a helpful assistant."),
ChatMessage(role=Role.USER, text="Hello"),
ChatMessage("system", ["You are a helpful assistant."]),
ChatMessage("user", ["Hello"]),
]
# Call the method
@@ -901,7 +900,7 @@ def test_prepare_options_with_image_content(mock_async_openai: MagicMock) -> Non
# Create message with image content
image_content = Content.from_uri(uri="https://example.com/image.jpg", media_type="image/jpeg")
messages = [ChatMessage(role=Role.USER, contents=[image_content])]
messages = [ChatMessage("user", [image_content])]
# Call the method
run_options, tool_results = chat_client._prepare_options(messages, {}) # type: ignore
@@ -1021,7 +1020,7 @@ async def test_get_response() -> None:
"It's a beautiful day for outdoor activities.",
)
)
messages.append(ChatMessage(role="user", text="What's the weather like today?"))
messages.append(ChatMessage("user", ["What's the weather like today?"]))
# Test that the client can be used to get a response
response = await openai_assistants_client.get_response(messages=messages)
@@ -1039,7 +1038,7 @@ async def test_get_response_tools() -> None:
assert isinstance(openai_assistants_client, ChatClientProtocol)
messages: list[ChatMessage] = []
messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?"))
messages.append(ChatMessage("user", ["What's the weather like in Seattle?"]))
# Test that the client can be used to get a response
response = await openai_assistants_client.get_response(
@@ -1067,7 +1066,7 @@ async def test_streaming() -> None:
"It's a beautiful day for outdoor activities.",
)
)
messages.append(ChatMessage(role="user", text="What's the weather like today?"))
messages.append(ChatMessage("user", ["What's the weather like today?"]))
# Test that the client can be used to get a response
response = openai_assistants_client.get_streaming_response(messages=messages)
@@ -1091,7 +1090,7 @@ async def test_streaming_tools() -> None:
assert isinstance(openai_assistants_client, ChatClientProtocol)
messages: list[ChatMessage] = []
messages.append(ChatMessage(role="user", text="What's the weather like in Seattle?"))
messages.append(ChatMessage("user", ["What's the weather like in Seattle?"]))
# Test that the client can be used to get a response
response = openai_assistants_client.get_streaming_response(
@@ -1119,7 +1118,7 @@ async def test_with_existing_assistant() -> None:
# First create an assistant to use in the test
async with OpenAIAssistantsClient(model_id=INTEGRATION_TEST_MODEL) as temp_client:
# Get the assistant ID by triggering assistant creation
messages = [ChatMessage(role="user", text="Hello")]
messages = [ChatMessage("user", ["Hello"])]
await temp_client.get_response(messages=messages)
assistant_id = temp_client.assistant_id
@@ -1130,7 +1129,7 @@ async def test_with_existing_assistant() -> None:
assert isinstance(openai_assistants_client, ChatClientProtocol)
assert openai_assistants_client.assistant_id == assistant_id
messages = [ChatMessage(role="user", text="What can you do?")]
messages = [ChatMessage("user", ["What can you do?"])]
# Test that the client can be used to get a response
response = await openai_assistants_client.get_response(messages=messages)
@@ -1149,7 +1148,7 @@ async def test_file_search() -> None:
assert isinstance(openai_assistants_client, ChatClientProtocol)
messages: list[ChatMessage] = []
messages.append(ChatMessage(role="user", text="What's the weather like today?"))
messages.append(ChatMessage("user", ["What's the weather like today?"]))
file_id, vector_store = await create_vector_store(openai_assistants_client)
response = await openai_assistants_client.get_response(
@@ -1175,7 +1174,7 @@ async def test_file_search_streaming() -> None:
assert isinstance(openai_assistants_client, ChatClientProtocol)
messages: list[ChatMessage] = []
messages.append(ChatMessage(role="user", text="What's the weather like today?"))
messages.append(ChatMessage("user", ["What's the weather like today?"]))
file_id, vector_store = await create_vector_store(openai_assistants_client)
response = openai_assistants_client.get_streaming_response(
@@ -154,7 +154,7 @@ def test_serialize_with_org_id(openai_unit_test_env: dict[str, str]) -> None:
async def test_content_filter_exception_handling(openai_unit_test_env: dict[str, str]) -> None:
"""Test that content filter errors are properly handled."""
client = OpenAIChatClient()
messages = [ChatMessage(role="user", text="test message")]
messages = [ChatMessage("user", ["test message"])]
# Create a mock BadRequestError with content_filter code
mock_response = MagicMock()
@@ -209,7 +209,7 @@ def get_weather(location: str) -> str:
async def test_exception_message_includes_original_error_details() -> None:
"""Test that exception messages include original error details in the new format."""
client = OpenAIChatClient(model_id="test-model", api_key="test-key")
messages = [ChatMessage(role="user", text="test message")]
messages = [ChatMessage("user", ["test message"])]
mock_response = MagicMock()
original_error_message = "Invalid API request format"
@@ -652,12 +652,12 @@ def test_function_approval_content_is_skipped_in_preparation(openai_unit_test_en
)
# Test that approval request is skipped
message_with_request = ChatMessage(role="assistant", contents=[approval_request])
message_with_request = ChatMessage("assistant", [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])
message_with_response = ChatMessage("user", [approval_response])
prepared_response = client._prepare_message_for_openai(message_with_response)
assert len(prepared_response) == 0 # Should be empty - approval content is skipped
@@ -752,7 +752,7 @@ def test_prepare_options_without_model_id(openai_unit_test_env: dict[str, str])
client = OpenAIChatClient()
client.model_id = None # Remove model_id
messages = [ChatMessage(role="user", text="test")]
messages = [ChatMessage("user", ["test"])]
with pytest.raises(ValueError, match="model_id must be a non-empty string"):
client._prepare_options(messages, {})
@@ -786,7 +786,7 @@ def test_prepare_options_with_instructions(openai_unit_test_env: dict[str, str])
"""Test that instructions are prepended as system message."""
client = OpenAIChatClient()
messages = [ChatMessage(role="user", text="Hello")]
messages = [ChatMessage("user", ["Hello"])]
options = {"instructions": "You are a helpful assistant."}
prepared_options = client._prepare_options(messages, options)
@@ -836,7 +836,7 @@ def test_tool_choice_required_with_function_name(openai_unit_test_env: dict[str,
"""Test that tool_choice with required mode and function name is correctly prepared."""
client = OpenAIChatClient()
messages = [ChatMessage(role="user", text="test")]
messages = [ChatMessage("user", ["test"])]
options = {
"tools": [get_weather],
"tool_choice": {"mode": "required", "required_function_name": "get_weather"},
@@ -854,7 +854,7 @@ def test_response_format_dict_passthrough(openai_unit_test_env: dict[str, str])
"""Test that response_format as dict is passed through directly."""
client = OpenAIChatClient()
messages = [ChatMessage(role="user", text="test")]
messages = [ChatMessage("user", ["test"])]
custom_format = {
"type": "json_schema",
"json_schema": {"name": "Test", "schema": {"type": "object"}},
@@ -894,7 +894,7 @@ def test_prepare_options_removes_parallel_tool_calls_when_no_tools(openai_unit_t
"""Test that parallel_tool_calls is removed when no tools are present."""
client = OpenAIChatClient()
messages = [ChatMessage(role="user", text="test")]
messages = [ChatMessage("user", ["test"])]
options = {"allow_multiple_tool_calls": True}
prepared_options = client._prepare_options(messages, options)
@@ -906,7 +906,7 @@ def test_prepare_options_removes_parallel_tool_calls_when_no_tools(openai_unit_t
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")]
messages = [ChatMessage("user", ["test"])]
# Create a mock error during streaming
mock_error = Exception("Streaming error")
@@ -1008,14 +1008,14 @@ async def test_integration_options(
# Prepare test message
if option_name.startswith("tools") or option_name.startswith("tool_choice"):
# Use weather-related prompt for tool tests
messages = [ChatMessage(role="user", text="What is the weather in Seattle?")]
messages = [ChatMessage("user", ["What is the weather in Seattle?"])]
elif option_name.startswith("response_format"):
# Use prompt that works well with structured output
messages = [ChatMessage(role="user", text="The weather in Seattle is sunny")]
messages.append(ChatMessage(role="user", text="What is the weather in Seattle?"))
messages = [ChatMessage("user", ["The weather in Seattle is sunny"])]
messages.append(ChatMessage("user", ["What is the weather in Seattle?"]))
else:
# Generic prompt for simple options
messages = [ChatMessage(role="user", text="Say 'Hello World' briefly.")]
messages = [ChatMessage("user", ["Say 'Hello World' briefly."])]
# Build options dict
options: dict[str, Any] = {option_name: option_value}
@@ -1032,7 +1032,7 @@ async def test_integration_options(
)
output_format = option_value if option_name.startswith("response_format") else None
response = await ChatResponse.from_chat_response_generator(response_gen, output_format_type=output_format)
response = await ChatResponse.from_update_generator(response_gen, output_format_type=output_format)
else:
# Test non-streaming mode
response = await client.get_response(
@@ -1080,7 +1080,7 @@ async def test_integration_web_search() -> None:
},
}
if streaming:
response = await ChatResponse.from_chat_response_generator(client.get_streaming_response(**content))
response = await ChatResponse.from_update_generator(client.get_streaming_response(**content))
else:
response = await client.get_response(**content)
@@ -1105,7 +1105,7 @@ async def test_integration_web_search() -> None:
},
}
if streaming:
response = await ChatResponse.from_chat_response_generator(client.get_streaming_response(**content))
response = await ChatResponse.from_update_generator(client.get_streaming_response(**content))
else:
response = await client.get_response(**content)
assert response.text is not None
@@ -69,7 +69,7 @@ async def test_cmc(
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_chat_completion_response
chat_history.append(ChatMessage(role="user", text="hello world"))
chat_history.append(ChatMessage("user", ["hello world"]))
openai_chat_completion = OpenAIChatClient()
await openai_chat_completion.get_response(messages=chat_history)
@@ -88,7 +88,7 @@ async def test_cmc_chat_options(
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_chat_completion_response
chat_history.append(ChatMessage(role="user", text="hello world"))
chat_history.append(ChatMessage("user", ["hello world"]))
openai_chat_completion = OpenAIChatClient()
await openai_chat_completion.get_response(
@@ -109,7 +109,7 @@ async def test_cmc_no_fcc_in_response(
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_chat_completion_response
chat_history.append(ChatMessage(role="user", text="hello world"))
chat_history.append(ChatMessage("user", ["hello world"]))
orig_chat_history = deepcopy(chat_history)
openai_chat_completion = OpenAIChatClient()
@@ -131,7 +131,7 @@ async def test_cmc_structured_output_no_fcc(
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_chat_completion_response
chat_history.append(ChatMessage(role="user", text="hello world"))
chat_history.append(ChatMessage("user", ["hello world"]))
# Define a mock response format
class Test(BaseModel):
@@ -153,7 +153,7 @@ async def test_scmc_chat_options(
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_streaming_chat_completion_response
chat_history.append(ChatMessage(role="user", text="hello world"))
chat_history.append(ChatMessage("user", ["hello world"]))
openai_chat_completion = OpenAIChatClient()
async for msg in openai_chat_completion.get_streaming_response(
@@ -178,7 +178,7 @@ async def test_cmc_general_exception(
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_chat_completion_response
chat_history.append(ChatMessage(role="user", text="hello world"))
chat_history.append(ChatMessage("user", ["hello world"]))
openai_chat_completion = OpenAIChatClient()
with pytest.raises(ServiceResponseException):
@@ -195,7 +195,7 @@ async def test_cmc_additional_properties(
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_chat_completion_response
chat_history.append(ChatMessage(role="user", text="hello world"))
chat_history.append(ChatMessage("user", ["hello world"]))
openai_chat_completion = OpenAIChatClient()
await openai_chat_completion.get_response(messages=chat_history, options={"reasoning_effort": "low"})
@@ -233,7 +233,7 @@ async def test_get_streaming(
stream = MagicMock(spec=AsyncStream)
stream.__aiter__.return_value = [content1, content2]
mock_create.return_value = stream
chat_history.append(ChatMessage(role="user", text="hello world"))
chat_history.append(ChatMessage("user", ["hello world"]))
orig_chat_history = deepcopy(chat_history)
openai_chat_completion = OpenAIChatClient()
@@ -272,7 +272,7 @@ async def test_get_streaming_singular(
stream = MagicMock(spec=AsyncStream)
stream.__aiter__.return_value = [content1, content2]
mock_create.return_value = stream
chat_history.append(ChatMessage(role="user", text="hello world"))
chat_history.append(ChatMessage("user", ["hello world"]))
orig_chat_history = deepcopy(chat_history)
openai_chat_completion = OpenAIChatClient()
@@ -311,7 +311,7 @@ async def test_get_streaming_structured_output_no_fcc(
stream = MagicMock(spec=AsyncStream)
stream.__aiter__.return_value = [content1, content2]
mock_create.return_value = stream
chat_history.append(ChatMessage(role="user", text="hello world"))
chat_history.append(ChatMessage("user", ["hello world"]))
# Define a mock response format
class Test(BaseModel):
@@ -334,7 +334,7 @@ async def test_get_streaming_no_fcc_in_response(
openai_unit_test_env: dict[str, str],
):
mock_create.return_value = mock_streaming_chat_completion_response
chat_history.append(ChatMessage(role="user", text="hello world"))
chat_history.append(ChatMessage("user", ["hello world"]))
orig_chat_history = deepcopy(chat_history)
openai_chat_completion = OpenAIChatClient()
@@ -360,7 +360,7 @@ async def test_get_streaming_no_stream(
mock_chat_completion_response: ChatCompletion, # AsyncStream[ChatCompletionChunk]?
):
mock_create.return_value = mock_chat_completion_response
chat_history.append(ChatMessage(role="user", text="hello world"))
chat_history.append(ChatMessage("user", ["hello world"]))
openai_chat_completion = OpenAIChatClient()
with pytest.raises(ServiceResponseException):
@@ -39,7 +39,6 @@ from agent_framework import (
HostedImageGenerationTool,
HostedMCPTool,
HostedWebSearchTool,
Role,
tool,
)
from agent_framework.exceptions import (
@@ -215,7 +214,7 @@ def test_get_response_with_all_parameters() -> None:
with pytest.raises(ServiceResponseException):
asyncio.run(
client.get_response(
messages=[ChatMessage(role="user", text="Test message")],
messages=[ChatMessage("user", ["Test message"])],
options={
"include": ["message.output_text.logprobs"],
"instructions": "You are a helpful assistant",
@@ -261,7 +260,7 @@ def test_web_search_tool_with_location() -> None:
with pytest.raises(ServiceResponseException):
asyncio.run(
client.get_response(
messages=[ChatMessage(role="user", text="What's the weather?")],
messages=[ChatMessage("user", ["What's the weather?"])],
options={"tools": [web_search_tool], "tool_choice": "auto"},
)
)
@@ -278,7 +277,7 @@ def test_file_search_tool_with_invalid_inputs() -> None:
with pytest.raises(ValueError, match="HostedFileSearchTool requires inputs to be of type"):
asyncio.run(
client.get_response(
messages=[ChatMessage(role="user", text="Search files")],
messages=[ChatMessage("user", ["Search files"])],
options={"tools": [file_search_tool]},
)
)
@@ -294,7 +293,7 @@ def test_code_interpreter_tool_variations() -> None:
with pytest.raises(ServiceResponseException):
asyncio.run(
client.get_response(
messages=[ChatMessage(role="user", text="Run some code")],
messages=[ChatMessage("user", ["Run some code"])],
options={"tools": [code_tool_empty]},
)
)
@@ -307,7 +306,7 @@ def test_code_interpreter_tool_variations() -> None:
with pytest.raises(ServiceResponseException):
asyncio.run(
client.get_response(
messages=[ChatMessage(role="user", text="Process these files")],
messages=[ChatMessage("user", ["Process these files"])],
options={"tools": [code_tool_with_files]},
)
)
@@ -327,7 +326,7 @@ def test_content_filter_exception() -> None:
with patch.object(client.client.responses, "create", side_effect=mock_error):
with pytest.raises(OpenAIContentFilterException) as exc_info:
asyncio.run(client.get_response(messages=[ChatMessage(role="user", text="Test message")]))
asyncio.run(client.get_response(messages=[ChatMessage("user", ["Test message"])]))
assert "content error" in str(exc_info.value)
@@ -343,7 +342,7 @@ def test_hosted_file_search_tool_validation() -> None:
with pytest.raises((ValueError, ServiceInvalidRequestError)):
asyncio.run(
client.get_response(
messages=[ChatMessage(role="user", text="Test")],
messages=[ChatMessage("user", ["Test"])],
options={"tools": [empty_file_search_tool]},
)
)
@@ -364,9 +363,9 @@ def test_chat_message_parsing_with_function_calls() -> None:
function_result = Content.from_function_result(call_id="test-call-id", result="Function executed successfully")
messages = [
ChatMessage(role="user", text="Call a function"),
ChatMessage(role="assistant", contents=[function_call]),
ChatMessage(role="tool", contents=[function_result]),
ChatMessage("user", ["Call a function"]),
ChatMessage("assistant", [function_call]),
ChatMessage("tool", [function_result]),
]
# This should exercise the message parsing logic - will fail due to invalid API key
@@ -392,7 +391,7 @@ async def test_response_format_parse_path() -> None:
with patch.object(client.client.responses, "parse", return_value=mock_parsed_response):
response = await client.get_response(
messages=[ChatMessage(role="user", text="Test message")],
messages=[ChatMessage("user", ["Test message"])],
options={"response_format": OutputStruct, "store": True},
)
assert response.response_id == "parsed_response_123"
@@ -419,7 +418,7 @@ async def test_response_format_parse_path_with_conversation_id() -> None:
with patch.object(client.client.responses, "parse", return_value=mock_parsed_response):
response = await client.get_response(
messages=[ChatMessage(role="user", text="Test message")],
messages=[ChatMessage("user", ["Test message"])],
options={"response_format": OutputStruct, "store": True},
)
assert response.response_id == "parsed_response_123"
@@ -442,7 +441,7 @@ async def test_bad_request_error_non_content_filter() -> None:
with patch.object(client.client.responses, "parse", side_effect=mock_error):
with pytest.raises(ServiceResponseException) as exc_info:
await client.get_response(
messages=[ChatMessage(role="user", text="Test message")],
messages=[ChatMessage("user", ["Test message"])],
options={"response_format": OutputStruct},
)
@@ -463,7 +462,7 @@ async def test_streaming_content_filter_exception_handling() -> None:
mock_create.side_effect.code = "content_filter"
with pytest.raises(OpenAIContentFilterException, match="service encountered a content error"):
response_stream = client.get_streaming_response(messages=[ChatMessage(role="user", text="Test")])
response_stream = client.get_streaming_response(messages=[ChatMessage("user", ["Test"])])
async for _ in response_stream:
break
@@ -658,7 +657,7 @@ def test_prepare_content_for_opentool_approval_response() -> None:
function_call=function_call,
)
result = client._prepare_content_for_openai(Role.ASSISTANT, approval_response, {})
result = client._prepare_content_for_openai("assistant", approval_response, {})
assert result["type"] == "mcp_approval_response"
assert result["approval_request_id"] == "approval_001"
@@ -675,7 +674,7 @@ def test_prepare_content_for_openai_error_content() -> None:
error_details="Invalid parameter",
)
result = client._prepare_content_for_openai(Role.ASSISTANT, error_content, {})
result = client._prepare_content_for_openai("assistant", error_content, {})
# ErrorContent should return empty dict (logged but not sent)
assert result == {}
@@ -693,7 +692,7 @@ def test_prepare_content_for_openai_usage_content() -> None:
}
)
result = client._prepare_content_for_openai(Role.ASSISTANT, usage_content, {})
result = client._prepare_content_for_openai("assistant", usage_content, {})
# UsageContent should return empty dict (logged but not sent)
assert result == {}
@@ -707,7 +706,7 @@ def test_prepare_content_for_openai_hosted_vector_store_content() -> None:
vector_store_id="vs_123",
)
result = client._prepare_content_for_openai(Role.ASSISTANT, vector_store_content, {})
result = client._prepare_content_for_openai("assistant", vector_store_content, {})
# HostedVectorStoreContent should return empty dict (logged but not sent)
assert result == {}
@@ -807,7 +806,7 @@ def test_prepare_message_for_openai_with_function_approval_response() -> None:
function_call=function_call,
)
message = ChatMessage(role="user", contents=[approval_response])
message = ChatMessage("user", [approval_response])
call_id_to_id: dict[str, str] = {}
result = client._prepare_message_for_openai(message, call_id_to_id)
@@ -829,7 +828,7 @@ def test_chat_message_with_error_content() -> None:
error_code="TEST_ERR",
)
message = ChatMessage(role="assistant", contents=[error_content])
message = ChatMessage("assistant", [error_content])
call_id_to_id: dict[str, str] = {}
result = client._prepare_message_for_openai(message, call_id_to_id)
@@ -854,7 +853,7 @@ def test_chat_message_with_usage_content() -> None:
}
)
message = ChatMessage(role="assistant", contents=[usage_content])
message = ChatMessage("assistant", [usage_content])
call_id_to_id: dict[str, str] = {}
result = client._prepare_message_for_openai(message, call_id_to_id)
@@ -877,7 +876,7 @@ def test_hosted_file_content_preparation() -> None:
name="document.pdf",
)
result = client._prepare_content_for_openai(Role.USER, hosted_file, {})
result = client._prepare_content_for_openai("user", hosted_file, {})
assert result["type"] == "input_file"
assert result["file_id"] == "file_abc123"
@@ -900,7 +899,7 @@ def test_function_approval_response_with_mcp_tool_call() -> None:
function_call=mcp_call,
)
result = client._prepare_content_for_openai(Role.ASSISTANT, approval_response, {})
result = client._prepare_content_for_openai("assistant", approval_response, {})
assert result["type"] == "mcp_approval_response"
assert result["approval_request_id"] == "approval_mcp_001"
@@ -1358,14 +1357,14 @@ async def test_end_to_end_mcp_approval_flow(span_exporter) -> None:
# Patch the create call to return the two mocked responses in sequence
with patch.object(client.client.responses, "create", side_effect=[mock_response1, mock_response2]) as mock_create:
# First call: get the approval request
response = await client.get_response(messages=[ChatMessage(role="user", text="Trigger approval")])
response = await client.get_response(messages=[ChatMessage("user", ["Trigger approval"])])
assert response.messages[0].contents[0].type == "function_approval_request"
req = response.messages[0].contents[0]
assert req.id == "approval-1"
# Build a user approval and send it (include required function_call)
approval = Content.from_function_approval_response(approved=True, id=req.id, function_call=req.function_call)
approval_message = ChatMessage(role="user", contents=[approval])
approval_message = ChatMessage("user", [approval])
_ = await client.get_response(messages=[approval_message])
# Ensure two calls were made and the second includes the mcp_approval_response
@@ -1469,7 +1468,7 @@ def test_streaming_response_basic_structure() -> None:
# Should get a valid ChatResponseUpdate structure
assert isinstance(response, ChatResponseUpdate)
assert response.role == Role.ASSISTANT
assert response.role == "assistant"
assert response.model_id == "test-model"
assert isinstance(response.contents, list)
assert response.raw_representation is mock_event
@@ -1620,7 +1619,7 @@ def test_streaming_annotation_added_with_unknown_type() -> None:
def test_service_response_exception_includes_original_error_details() -> None:
"""Test that ServiceResponseException messages include original error details in the new format."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
messages = [ChatMessage(role="user", text="test message")]
messages = [ChatMessage("user", ["test message"])]
mock_response = MagicMock()
original_error_message = "Request rate limit exceeded"
@@ -1645,7 +1644,7 @@ def test_service_response_exception_includes_original_error_details() -> None:
def test_get_streaming_response_with_response_format() -> None:
"""Test get_streaming_response with response_format."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
messages = [ChatMessage(role="user", text="Test streaming with format")]
messages = [ChatMessage("user", ["Test streaming with format"])]
# It will fail due to invalid API key, but exercises the code path
with pytest.raises(ServiceResponseException):
@@ -1667,7 +1666,7 @@ def test_prepare_content_for_openai_image_content() -> None:
media_type="image/jpeg",
additional_properties={"detail": "high", "file_id": "file_123"},
)
result = client._prepare_content_for_openai(Role.USER, image_content_with_detail, {}) # type: ignore
result = client._prepare_content_for_openai("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"
@@ -1675,7 +1674,7 @@ def test_prepare_content_for_openai_image_content() -> None:
# Test image content without additional properties (defaults)
image_content_basic = Content.from_uri(uri="https://example.com/basic.png", media_type="image/png")
result = client._prepare_content_for_openai(Role.USER, image_content_basic, {}) # type: ignore
result = client._prepare_content_for_openai("user", image_content_basic, {}) # type: ignore
assert result["type"] == "input_image"
assert result["detail"] == "auto"
assert result["file_id"] is None
@@ -1687,14 +1686,14 @@ def test_prepare_content_for_openai_audio_content() -> None:
# Test WAV audio content
wav_content = Content.from_uri(uri="data:audio/wav;base64,abc123", media_type="audio/wav")
result = client._prepare_content_for_openai(Role.USER, wav_content, {}) # type: ignore
result = client._prepare_content_for_openai("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 = Content.from_uri(uri="data:audio/mp3;base64,def456", media_type="audio/mp3")
result = client._prepare_content_for_openai(Role.USER, mp3_content, {}) # type: ignore
result = client._prepare_content_for_openai("user", mp3_content, {}) # type: ignore
assert result["type"] == "input_audio"
assert result["input_audio"]["format"] == "mp3"
@@ -1705,12 +1704,12 @@ def test_prepare_content_for_openai_unsupported_content() -> None:
# Test unsupported audio format
unsupported_audio = Content.from_uri(uri="data:audio/ogg;base64,ghi789", media_type="audio/ogg")
result = client._prepare_content_for_openai(Role.USER, unsupported_audio, {}) # type: ignore
result = client._prepare_content_for_openai("user", unsupported_audio, {}) # type: ignore
assert result == {}
# Test non-media content
text_uri_content = Content.from_uri(uri="https://example.com/document.txt", media_type="text/plain")
result = client._prepare_content_for_openai(Role.USER, text_uri_content, {}) # type: ignore
result = client._prepare_content_for_openai("user", text_uri_content, {}) # type: ignore
assert result == {}
@@ -1775,7 +1774,7 @@ def test_prepare_content_for_openai_text_reasoning_comprehensive() -> None:
"encrypted_content": "secure_data_456",
},
)
result = client._prepare_content_for_openai(Role.ASSISTANT, comprehensive_reasoning, {}) # type: ignore
result = client._prepare_content_for_openai("assistant", comprehensive_reasoning, {}) # type: ignore
assert result["type"] == "reasoning"
assert result["summary"]["text"] == "Comprehensive reasoning summary"
assert result["status"] == "in_progress"
@@ -2091,7 +2090,7 @@ def test_parse_response_from_openai_image_generation_fallback():
async def test_prepare_options_store_parameter_handling() -> None:
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
messages = [ChatMessage(role="user", text="Test message")]
messages = [ChatMessage("user", ["Test message"])]
test_conversation_id = "test-conversation-123"
chat_options = ChatOptions(store=True, conversation_id=test_conversation_id)
@@ -2117,7 +2116,7 @@ async def test_prepare_options_store_parameter_handling() -> None:
async def test_conversation_id_precedence_kwargs_over_options() -> None:
"""When both kwargs and options contain conversation_id, kwargs wins."""
client = OpenAIResponsesClient(model_id="test-model", api_key="test-key")
messages = [ChatMessage(role="user", text="Hello")]
messages = [ChatMessage("user", ["Hello"])]
# options has a stale response id, kwargs carries the freshest one
opts = {"conversation_id": "resp_old_123"}
@@ -2224,14 +2223,14 @@ async def test_integration_options(
# Prepare test message
if option_name.startswith("tools") or option_name.startswith("tool_choice"):
# Use weather-related prompt for tool tests
messages = [ChatMessage(role="user", text="What is the weather in Seattle?")]
messages = [ChatMessage("user", ["What is the weather in Seattle?"])]
elif option_name.startswith("response_format"):
# Use prompt that works well with structured output
messages = [ChatMessage(role="user", text="The weather in Seattle is sunny")]
messages.append(ChatMessage(role="user", text="What is the weather in Seattle?"))
messages = [ChatMessage("user", ["The weather in Seattle is sunny"])]
messages.append(ChatMessage("user", ["What is the weather in Seattle?"]))
else:
# Generic prompt for simple options
messages = [ChatMessage(role="user", text="Say 'Hello World' briefly.")]
messages = [ChatMessage("user", ["Say 'Hello World' briefly."])]
# Build options dict
options: dict[str, Any] = {option_name: option_value}
@@ -2248,7 +2247,7 @@ async def test_integration_options(
)
output_format = option_value if option_name.startswith("response_format") else None
response = await ChatResponse.from_chat_response_generator(response_gen, output_format_type=output_format)
response = await ChatResponse.from_update_generator(response_gen, output_format_type=output_format)
else:
# Test non-streaming mode
response = await openai_responses_client.get_response(
@@ -2296,7 +2295,7 @@ async def test_integration_web_search() -> None:
},
}
if streaming:
response = await ChatResponse.from_chat_response_generator(client.get_streaming_response(**content))
response = await ChatResponse.from_update_generator(client.get_streaming_response(**content))
else:
response = await client.get_response(**content)
@@ -2321,7 +2320,7 @@ async def test_integration_web_search() -> None:
},
}
if streaming:
response = await ChatResponse.from_chat_response_generator(client.get_streaming_response(**content))
response = await ChatResponse.from_update_generator(client.get_streaming_response(**content))
else:
response = await client.get_response(**content)
assert response.text is not None