|
- """
- Comprehensive tests for GPT-5 specific features in AutoGen.
-
- This test suite validates:
- - GPT-5 model recognition and configuration
- - Custom tools functionality (freeform text input)
- - Grammar constraints for custom tools
- - Reasoning effort parameter control
- - Verbosity parameter control
- - Preambles support
- - Allowed tools parameter
- - Responses API client implementation
- - Chain-of-thought preservation across turns
-
- Tests use mocking to avoid actual API calls while validating
- that all GPT-5 features are properly integrated and functional.
- """
-
- from typing import Any, Dict, List, cast
- from unittest.mock import AsyncMock, patch
-
- import pytest
- from autogen_core import CancellationToken
- from autogen_core.models import CreateResult, UserMessage
- from autogen_core.tools import BaseCustomTool, CustomToolFormat
- from autogen_ext.models.openai import (
- OpenAIChatCompletionClient,
- OpenAIResponsesAPIClient,
- )
- from autogen_ext.models.openai._model_info import get_info as get_model_info
- from autogen_ext.models.openai._openai_client import convert_tools
- from openai.types.chat.chat_completion import ChatCompletion, Choice
- from openai.types.chat.chat_completion_message import ChatCompletionMessage
- from openai.types.chat.chat_completion_message_function_tool_call import (
- ChatCompletionMessageFunctionToolCall as ChatCompletionMessageToolCall,
- )
- from openai.types.completion_usage import CompletionUsage
- from pydantic import BaseModel
-
-
- class CodeExecResult(BaseModel):
- result: str
-
-
- class TestCodeExecutorTool(BaseCustomTool[CodeExecResult]):
- """Test implementation of GPT-5 custom tool for code execution."""
-
- def __init__(self) -> None:
- super().__init__(
- return_type=CodeExecResult,
- name="code_exec",
- description="Executes arbitrary Python code and returns the result",
- )
-
- async def run(self, input_text: str, cancellation_token: CancellationToken) -> CodeExecResult:
- return CodeExecResult(result=f"Executed: {input_text}")
-
-
- class SQLResult(BaseModel):
- result: str
-
-
- class TestSQLTool(BaseCustomTool[SQLResult]):
- """Test implementation of GPT-5 custom tool with grammar constraints."""
-
- def __init__(self) -> None:
- sql_grammar: CustomToolFormat = {
- "type": "grammar",
- "syntax": "lark",
- "definition": """
- start: select_statement
- select_statement: "SELECT" column_list "FROM" table_name ("WHERE" condition)?
- column_list: column ("," column)*
- column: IDENTIFIER
- table_name: IDENTIFIER
- condition: column ">" NUMBER
- IDENTIFIER: /[a-zA-Z_][a-zA-Z0-9_]*/
- NUMBER: /[0-9]+/
- %import common.WS
- %ignore WS
- """,
- }
-
- super().__init__(
- return_type=SQLResult,
- name="sql_query",
- description="Execute SQL queries with grammar validation",
- format=sql_grammar,
- )
-
- async def run(self, input_text: str, cancellation_token: CancellationToken) -> SQLResult:
- return SQLResult(result=f"SQL Result: {input_text}")
-
-
- class TestGPT5ModelRecognition:
- """Test GPT-5 model definitions and capabilities."""
-
- def test_gpt5_model_info(self) -> None:
- """Test that GPT-5 models are properly recognized and configured."""
- gpt5_info = get_model_info("gpt-5")
- assert gpt5_info["vision"] is True
- assert gpt5_info["function_calling"] is True
- assert gpt5_info["json_output"] is True
- assert gpt5_info["structured_output"] is True
-
- gpt5_mini_info = get_model_info("gpt-5-mini")
- assert gpt5_mini_info["vision"] is True
- assert gpt5_mini_info["function_calling"] is True
-
- gpt5_nano_info = get_model_info("gpt-5-nano")
- assert gpt5_nano_info["vision"] is True
- assert gpt5_nano_info["function_calling"] is True
-
- def test_gpt5_token_limits(self) -> None:
- """Test GPT-5 models have correct token limits."""
- from autogen_ext.models.openai._model_info import get_token_limit
-
- assert get_token_limit("gpt-5") == 400000
- assert get_token_limit("gpt-5-mini") == 400000
- assert get_token_limit("gpt-5-nano") == 400000
-
-
- class TestCustomToolsIntegration:
- """Test GPT-5 custom tools functionality."""
-
- def test_custom_tool_schema_generation(self) -> None:
- """Test custom tool schema generation."""
- code_tool = TestCodeExecutorTool()
- schema = code_tool.schema
-
- assert schema["name"] == "code_exec"
- assert schema.get("description", "") == "Executes arbitrary Python code and returns the result"
- assert "format" not in schema # No grammar constraints
-
- def test_custom_tool_with_grammar_schema(self) -> None:
- """Test custom tool with grammar constraints."""
- sql_tool = TestSQLTool()
- schema = sql_tool.schema
-
- assert schema["name"] == "sql_query"
- assert "format" in schema
- fmt_any = schema.get("format")
- assert isinstance(fmt_any, dict)
- assert fmt_any.get("type") == "grammar"
- assert fmt_any.get("syntax") == "lark"
- assert isinstance(fmt_any.get("definition"), str) and "SELECT" in fmt_any.get("definition", "")
-
- def test_convert_custom_tools(self) -> None:
- """Test conversion of custom tools to OpenAI API format."""
- code_tool = TestCodeExecutorTool()
- sql_tool = TestSQLTool()
-
- converted = convert_tools([code_tool, sql_tool])
-
- assert len(converted) == 2
-
- # Check code tool conversion
- code_tool_param = next(cast(Dict[str, Any], t) for t in converted if cast(Dict[str, Any], t).get("custom", {}).get("name") == "code_exec")
- assert str(code_tool_param.get("type")) == "custom"
- assert "format" not in code_tool_param.get("custom", {})
-
- # Check SQL tool conversion with grammar
- sql_tool_param = next(cast(Dict[str, Any], t) for t in converted if cast(Dict[str, Any], t).get("custom", {}).get("name") == "sql_query")
- assert str(sql_tool_param.get("type")) == "custom"
- assert "format" in sql_tool_param.get("custom", {})
- assert sql_tool_param.get("custom", {}).get("format", {}).get("type") == "grammar"
-
- async def test_custom_tool_execution(self) -> None:
- """Test custom tool execution."""
- code_tool = TestCodeExecutorTool()
-
- result = await code_tool.run("print('hello world')", CancellationToken())
- assert result.result == "Executed: print('hello world')"
-
- result_via_freeform = await code_tool.run_freeform("x = 2 + 2", CancellationToken())
- assert result_via_freeform == "Executed: x = 2 + 2"
-
-
- class TestGPT5Parameters:
- """Test GPT-5 specific parameters."""
-
- @pytest.fixture
- def mock_openai_client(self) -> Any:
- """Mock OpenAI client for testing."""
- with patch("autogen_ext.models.openai._openai_client._openai_client_from_config") as mock:
- mock_client = AsyncMock()
- mock_client.chat.completions.create = AsyncMock()
- mock.return_value = mock_client
- yield mock_client
-
- @pytest.fixture
- def client(self, mock_openai_client: Any) -> OpenAIChatCompletionClient:
- """Create test client with mocked OpenAI client."""
- return OpenAIChatCompletionClient(model="gpt-5", api_key="test-key")
-
- async def test_reasoning_effort_parameter(
- self, client: OpenAIChatCompletionClient, mock_openai_client: Any
- ) -> None:
- """Test reasoning_effort parameter is properly passed."""
- # Mock successful API response
- mock_response = ChatCompletion(
- id="test-id",
- object="chat.completion",
- created=1234567890,
- model="gpt-5",
- choices=[
- Choice(
- index=0,
- message=ChatCompletionMessage(role="assistant", content="Test response"),
- finish_reason="stop",
- )
- ],
- usage=CompletionUsage(prompt_tokens=10, completion_tokens=20, total_tokens=30),
- )
- mock_openai_client.chat.completions.create.return_value = mock_response
-
- # Test different reasoning efforts
- for effort in ["minimal", "low", "medium", "high"]:
- await client.create(messages=[UserMessage(content="Test message", source="user")], reasoning_effort=effort) # type: ignore[arg-type]
-
- # Verify parameter was passed correctly
- call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
- assert call_kwargs["reasoning_effort"] == effort
-
- async def test_verbosity_parameter(self, client: OpenAIChatCompletionClient, mock_openai_client: Any) -> None:
- """Test verbosity parameter is properly passed."""
- mock_response = ChatCompletion(
- id="test-id",
- object="chat.completion",
- created=1234567890,
- model="gpt-5",
- choices=[
- Choice(
- index=0,
- message=ChatCompletionMessage(role="assistant", content="Test response"),
- finish_reason="stop",
- )
- ],
- usage=CompletionUsage(prompt_tokens=10, completion_tokens=20, total_tokens=30),
- )
- mock_openai_client.chat.completions.create.return_value = mock_response
-
- # Test different verbosity levels
- for verbosity in ["low", "medium", "high"]:
- await client.create(messages=[UserMessage(content="Test message", source="user")], verbosity=verbosity) # type: ignore[arg-type]
-
- call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
- assert call_kwargs["verbosity"] == verbosity
-
- async def test_preambles_parameter(self, client: OpenAIChatCompletionClient, mock_openai_client: Any) -> None:
- """Test preambles parameter is properly passed."""
- mock_response = ChatCompletion(
- id="test-id",
- object="chat.completion",
- created=1234567890,
- model="gpt-5",
- choices=[
- Choice(
- index=0,
- message=ChatCompletionMessage(role="assistant", content="Test response"),
- finish_reason="stop",
- )
- ],
- usage=CompletionUsage(prompt_tokens=10, completion_tokens=20, total_tokens=30),
- )
- mock_openai_client.chat.completions.create.return_value = mock_response
-
- # Test preambles enabled
- await client.create(messages=[UserMessage(content="Test message", source="user")], preambles=True)
-
- call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
- assert call_kwargs["preambles"] is True
-
- # Test preambles disabled
- await client.create(messages=[UserMessage(content="Test message", source="user")], preambles=False)
-
- call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
- assert call_kwargs["preambles"] is False
-
- async def test_combined_gpt5_parameters(self, client: OpenAIChatCompletionClient, mock_openai_client: Any) -> None:
- """Test multiple GPT-5 parameters used together."""
- mock_response = ChatCompletion(
- id="test-id",
- object="chat.completion",
- created=1234567890,
- model="gpt-5",
- choices=[
- Choice(
- index=0,
- message=ChatCompletionMessage(role="assistant", content="Test response"),
- finish_reason="stop",
- )
- ],
- usage=CompletionUsage(prompt_tokens=10, completion_tokens=20, total_tokens=30),
- )
- mock_openai_client.chat.completions.create.return_value = mock_response
-
- await client.create(
- messages=[UserMessage(content="Test message", source="user")],
- reasoning_effort="high",
- verbosity="medium",
- preambles=True,
- )
-
- call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
- assert call_kwargs["reasoning_effort"] == "high"
- assert call_kwargs["verbosity"] == "medium"
- assert call_kwargs["preambles"] is True
-
-
- class TestAllowedToolsFeature:
- """Test GPT-5 allowed_tools parameter for restricting tool usage."""
-
- @pytest.fixture
- def mock_openai_client(self) -> Any:
- with patch("autogen_ext.models.openai._openai_client._openai_client_from_config") as mock:
- mock_client = AsyncMock()
- mock_client.chat.completions.create = AsyncMock()
- mock.return_value = mock_client
- yield mock_client
-
- @pytest.fixture
- def client(self, mock_openai_client: Any) -> OpenAIChatCompletionClient:
- return OpenAIChatCompletionClient(model="gpt-5", api_key="test-key")
-
- async def test_allowed_tools_restriction(self, client: OpenAIChatCompletionClient, mock_openai_client: Any) -> None:
- """Test allowed_tools parameter restricts model to specific tools."""
- from autogen_core.tools import FunctionTool
-
- def safe_calc(x: int, y: int) -> int:
- return x + y
-
- def dangerous_exec(code: str) -> str:
- return f"Would execute: {code}"
-
- calc_tool = FunctionTool(safe_calc, description="Safe calculator")
- exec_tool = FunctionTool(dangerous_exec, description="Code executor")
- code_tool = TestCodeExecutorTool()
-
- from autogen_core.tools import Tool as _Tool, ToolSchema as _ToolSchema
- from autogen_core.tools import CustomTool as _CustomTool, CustomToolSchema as _CustomToolSchema
-
- all_tools: List[_Tool | _ToolSchema | _CustomTool | _CustomToolSchema] = [
- cast(_Tool, calc_tool),
- cast(_Tool, exec_tool),
- cast(_CustomTool, code_tool),
- ]
- safe_tools: List[_Tool | _CustomTool | str] = [cast(_Tool, calc_tool)] # Only allow calculator
-
- mock_response = ChatCompletion(
- id="test-id",
- object="chat.completion",
- created=1234567890,
- model="gpt-5",
- choices=[
- Choice(
- index=0,
- message=ChatCompletionMessage(role="assistant", content="Test response"),
- finish_reason="stop",
- )
- ],
- usage=CompletionUsage(prompt_tokens=10, completion_tokens=20, total_tokens=30),
- )
- mock_openai_client.chat.completions.create.return_value = mock_response
-
- await client.create(
- messages=[UserMessage(content="Help with math and coding", source="user")],
- tools=all_tools,
- allowed_tools=safe_tools,
- tool_choice="auto",
- )
-
- call_kwargs_any: Any = mock_openai_client.chat.completions.create.call_args[1]
-
- # Verify allowed_tools structure was created
- call_kwargs: Dict[str, Any] = cast(Dict[str, Any], call_kwargs_any)
- assert "tool_choice" in call_kwargs
- tool_choice_val: Any = call_kwargs.get("tool_choice")
-
- if isinstance(tool_choice_val, dict):
- tc: Dict[str, Any] = cast(Dict[str, Any], tool_choice_val)
- if str(tc.get("type", "")) == "allowed_tools":
- mode_val: str = str(tc.get("mode", ""))
- assert mode_val == "auto"
- tools_seq: List[Any] = list(cast(List[Any] | tuple[Any, ...], tc.get("tools", [])))
- tools_list: List[Dict[str, Any]] = [t for t in tools_seq if isinstance(t, dict)]
- allowed_tool_names: List[str] = [str(t.get("name", "")) for t in tools_list]
- assert "safe_calc" in allowed_tool_names
- assert "dangerous_exec" not in allowed_tool_names
- assert "code_exec" not in allowed_tool_names
-
-
- class TestResponsesAPIClient:
- """Test the dedicated Responses API client for GPT-5."""
-
- @pytest.fixture
- def mock_openai_client(self) -> Any:
- with patch("autogen_ext.models.openai._responses_client._openai_client_from_config") as mock:
- mock_client = AsyncMock()
- mock_client.responses.create = AsyncMock()
- mock.return_value = mock_client
- yield mock_client
-
- @pytest.fixture
- def responses_client(self, mock_openai_client: Any) -> OpenAIResponsesAPIClient:
- return OpenAIResponsesAPIClient(model="gpt-5", api_key="test-key")
-
- async def test_responses_api_basic_call(
- self, responses_client: OpenAIResponsesAPIClient, mock_openai_client: Any
- ) -> None:
- """Test basic Responses API call structure."""
- mock_response = {
- "id": "resp-123",
- "choices": [{"message": {"content": "Response content"}, "finish_reason": "stop"}],
- "usage": {"prompt_tokens": 10, "completion_tokens": 20},
- }
- mock_openai_client.responses.create.return_value = mock_response
-
- result = await responses_client.create(input="Test input message", reasoning_effort="medium", verbosity="high")
-
- assert isinstance(result, CreateResult)
- assert result.content == "Response content"
- assert result.usage.prompt_tokens == 10
- assert result.usage.completion_tokens == 20
-
- async def test_responses_api_with_cot_preservation(
- self, responses_client: OpenAIResponsesAPIClient, mock_openai_client: Any
- ) -> None:
- """Test chain-of-thought preservation between turns."""
- # First turn
- mock_response1 = {
- "id": "resp-123",
- "choices": [{"message": {"content": "First response"}, "finish_reason": "stop"}],
- "usage": {"prompt_tokens": 10, "completion_tokens": 20},
- "reasoning_items": [{"type": "reasoning", "content": "Initial reasoning"}],
- }
- mock_openai_client.responses.create.return_value = mock_response1
-
- result1 = await responses_client.create(input="First question", reasoning_effort="high")
-
- # Second turn with preserved CoT
- mock_response2 = {
- "id": "resp-124",
- "choices": [{"message": {"content": "Follow-up response"}, "finish_reason": "stop"}],
- "usage": {"prompt_tokens": 5, "completion_tokens": 15}, # Lower usage due to CoT reuse
- }
- mock_openai_client.responses.create.return_value = mock_response2
-
- result2 = await responses_client.create(
- input="Follow-up question",
- previous_response_id=result1.response_id, # type: ignore
- reasoning_effort="low", # Can use lower effort
- )
-
- # Verify previous_response_id was passed
- call_kwargs = mock_openai_client.responses.create.call_args[1]
- assert call_kwargs["previous_response_id"] == "resp-123"
- assert call_kwargs["reasoning"]["effort"] == "low"
- assert result2.content == "Follow-up response"
-
- async def test_responses_api_with_custom_tools(
- self, responses_client: OpenAIResponsesAPIClient, mock_openai_client: Any
- ) -> None:
- """Test Responses API with GPT-5 custom tools."""
- code_tool = TestCodeExecutorTool()
-
- mock_response = {
- "id": "resp-125",
- "choices": [
- {
- "message": {
- "content": "I'll execute the code for you.",
- "tool_calls": [
- {"id": "call-456", "custom": {"name": "code_exec", "input": "print('Hello GPT-5')"}}
- ],
- },
- "finish_reason": "tool_calls",
- }
- ],
- "usage": {"prompt_tokens": 15, "completion_tokens": 25},
- }
- mock_openai_client.responses.create.return_value = mock_response
-
- result = await responses_client.create(
- input="Run this Python code: print('Hello GPT-5')", tools=[code_tool], preambles=True
- )
-
- assert isinstance(result.content, list)
- assert len(result.content) == 1
- assert result.content[0].name == "code_exec"
- assert result.content[0].arguments == "print('Hello GPT-5')"
- assert result.thought == "I'll execute the code for you." # Preamble text
-
-
- class TestGPT5IntegrationScenarios:
- """Test realistic GPT-5 usage scenarios."""
-
- @pytest.fixture
- def mock_openai_client(self) -> Any:
- with patch("autogen_ext.models.openai._openai_client._openai_client_from_config") as mock:
- mock_client = AsyncMock()
- mock_client.chat.completions.create = AsyncMock()
- mock.return_value = mock_client
- yield mock_client
-
- @pytest.fixture
- def client(self, mock_openai_client: Any) -> OpenAIChatCompletionClient:
- return OpenAIChatCompletionClient(model="gpt-5", api_key="test-key")
-
- async def test_code_analysis_with_custom_tools(
- self, client: OpenAIChatCompletionClient, mock_openai_client: Any
- ) -> None:
- """Test GPT-5 analyzing and executing code with custom tools."""
- code_tool = TestCodeExecutorTool()
- sql_tool = TestSQLTool()
-
- mock_response = ChatCompletion(
- id="test-id",
- object="chat.completion",
- created=1234567890,
- model="gpt-5",
- choices=[
- Choice(
- index=0,
- message=ChatCompletionMessage(
- role="assistant",
- content="I need to analyze this code and run it.",
- tool_calls=[
- ChatCompletionMessageToolCall(
- id="call-123",
- type="custom", # type: ignore
- custom={ # type: ignore
- "name": "code_exec",
- "input": "def fibonacci(n):\n return n if n <= 1 else fibonacci(n-1) + fibonacci(n-2)\nprint(fibonacci(10))",
- },
- )
- ],
- ),
- finish_reason="tool_calls",
- )
- ],
- usage=CompletionUsage(prompt_tokens=50, completion_tokens=30, total_tokens=80),
- )
- mock_openai_client.chat.completions.create.return_value = mock_response
-
- # Tools typed to expected union for create
- tools_param = [code_tool, sql_tool]
- result = await client.create(
- messages=[UserMessage(content="Analyze this fibonacci implementation and run it for n=10", source="user")],
- tools=tools_param,
- reasoning_effort="medium", # type: ignore[arg-type]
- verbosity="low", # type: ignore[arg-type]
- preambles=True,
- )
-
- # Verify GPT-5 parameters were passed
- call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
- assert call_kwargs["reasoning_effort"] == "medium"
- assert call_kwargs["verbosity"] == "low"
- assert call_kwargs["preambles"] is True
-
- # Verify tools were converted properly
- assert "tools" in call_kwargs
- tools = call_kwargs["tools"]
- assert len(tools) == 2
-
- # Check that result contains tool call
- assert isinstance(result.content, list)
- assert len(result.content) == 1
- assert result.thought == "I need to analyze this code and run it."
-
- async def test_multi_modal_with_reasoning_control(
- self, client: OpenAIChatCompletionClient, mock_openai_client: Any
- ) -> None:
- """Test GPT-5 with vision and reasoning control."""
- import io
-
- from autogen_core import Image
- from PIL import Image as PILImage
-
- # Create a simple test image
- pil_image = PILImage.new("RGB", (100, 100), color="red")
- image_bytes = io.BytesIO()
- pil_image.save(image_bytes, format="PNG")
- image_bytes.seek(0)
-
- test_image = Image.from_pil(pil_image)
-
- mock_response = ChatCompletion(
- id="test-id",
- object="chat.completion",
- created=1234567890,
- model="gpt-5",
- choices=[
- Choice(
- index=0,
- message=ChatCompletionMessage(
- role="assistant", content="I can see this is a red square image. Let me analyze it further..."
- ),
- finish_reason="stop",
- )
- ],
- usage=CompletionUsage(prompt_tokens=100, completion_tokens=40, total_tokens=140),
- )
- mock_openai_client.chat.completions.create.return_value = mock_response
-
- result = await client.create(
- messages=[UserMessage(content=["What do you see in this image?", test_image], source="user")],
- reasoning_effort="high",
- verbosity="high",
- )
-
- assert result.content == "I can see this is a red square image. Let me analyze it further..."
-
- # Verify vision-related processing occurred
- call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
- assert call_kwargs["reasoning_effort"] == "high"
- assert call_kwargs["verbosity"] == "high"
-
-
- @pytest.mark.asyncio
- async def test_gpt5_error_handling() -> None:
- """Test proper error handling for GPT-5 specific scenarios."""
-
- # Test invalid reasoning effort
- with pytest.raises(ValueError): # Type validation should catch this
- _client = OpenAIChatCompletionClient(model="gpt-5", api_key="test-key")
- # This should be caught by type checking, but test anyway
-
- # Test model without GPT-5 capabilities using GPT-5 features
- with patch("autogen_ext.models.openai._openai_client._openai_client_from_config") as mock:
- mock_client = AsyncMock()
- mock.return_value = mock_client
-
- # Test with non-GPT-5 model
- old_model_client = OpenAIChatCompletionClient(model="gpt-4", api_key="test-key")
-
- # GPT-4 should still accept these parameters (they'll be ignored by the API)
- mock_client.chat.completions.create.return_value = ChatCompletion(
- id="test",
- object="chat.completion",
- created=1234567890,
- model="gpt-4",
- choices=[],
- usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
- )
-
- # This should work but parameters won't have any effect
- await old_model_client.create(
- messages=[UserMessage(content="Test", source="user")],
- reasoning_effort="high", # Will be passed but ignored
- preambles=True,
- )
-
-
- if __name__ == "__main__":
- # Run basic validation tests
- pytest.main([__file__, "-v"])
|