You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

test_gpt5_features.py 27 kB

11 months ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. """
  2. Comprehensive tests for GPT-5 specific features in AutoGen.
  3. This test suite validates:
  4. - GPT-5 model recognition and configuration
  5. - Custom tools functionality (freeform text input)
  6. - Grammar constraints for custom tools
  7. - Reasoning effort parameter control
  8. - Verbosity parameter control
  9. - Preambles support
  10. - Allowed tools parameter
  11. - Responses API client implementation
  12. - Chain-of-thought preservation across turns
  13. Tests use mocking to avoid actual API calls while validating
  14. that all GPT-5 features are properly integrated and functional.
  15. """
  16. from typing import Any, Dict, List, cast
  17. from unittest.mock import AsyncMock, patch
  18. import pytest
  19. from autogen_core import CancellationToken
  20. from autogen_core.models import CreateResult, UserMessage
  21. from autogen_core.tools import BaseCustomTool, CustomToolFormat
  22. from autogen_ext.models.openai import (
  23. OpenAIChatCompletionClient,
  24. OpenAIResponsesAPIClient,
  25. )
  26. from autogen_ext.models.openai._model_info import get_info as get_model_info
  27. from autogen_ext.models.openai._openai_client import convert_tools
  28. from openai.types.chat.chat_completion import ChatCompletion, Choice
  29. from openai.types.chat.chat_completion_message import ChatCompletionMessage
  30. from openai.types.chat.chat_completion_message_function_tool_call import (
  31. ChatCompletionMessageFunctionToolCall as ChatCompletionMessageToolCall,
  32. )
  33. from openai.types.completion_usage import CompletionUsage
  34. from pydantic import BaseModel
  35. class CodeExecResult(BaseModel):
  36. result: str
  37. class TestCodeExecutorTool(BaseCustomTool[CodeExecResult]):
  38. """Test implementation of GPT-5 custom tool for code execution."""
  39. def __init__(self) -> None:
  40. super().__init__(
  41. return_type=CodeExecResult,
  42. name="code_exec",
  43. description="Executes arbitrary Python code and returns the result",
  44. )
  45. async def run(self, input_text: str, cancellation_token: CancellationToken) -> CodeExecResult:
  46. return CodeExecResult(result=f"Executed: {input_text}")
  47. class SQLResult(BaseModel):
  48. result: str
  49. class TestSQLTool(BaseCustomTool[SQLResult]):
  50. """Test implementation of GPT-5 custom tool with grammar constraints."""
  51. def __init__(self) -> None:
  52. sql_grammar: CustomToolFormat = {
  53. "type": "grammar",
  54. "syntax": "lark",
  55. "definition": """
  56. start: select_statement
  57. select_statement: "SELECT" column_list "FROM" table_name ("WHERE" condition)?
  58. column_list: column ("," column)*
  59. column: IDENTIFIER
  60. table_name: IDENTIFIER
  61. condition: column ">" NUMBER
  62. IDENTIFIER: /[a-zA-Z_][a-zA-Z0-9_]*/
  63. NUMBER: /[0-9]+/
  64. %import common.WS
  65. %ignore WS
  66. """,
  67. }
  68. super().__init__(
  69. return_type=SQLResult,
  70. name="sql_query",
  71. description="Execute SQL queries with grammar validation",
  72. format=sql_grammar,
  73. )
  74. async def run(self, input_text: str, cancellation_token: CancellationToken) -> SQLResult:
  75. return SQLResult(result=f"SQL Result: {input_text}")
  76. class TestGPT5ModelRecognition:
  77. """Test GPT-5 model definitions and capabilities."""
  78. def test_gpt5_model_info(self) -> None:
  79. """Test that GPT-5 models are properly recognized and configured."""
  80. gpt5_info = get_model_info("gpt-5")
  81. assert gpt5_info["vision"] is True
  82. assert gpt5_info["function_calling"] is True
  83. assert gpt5_info["json_output"] is True
  84. assert gpt5_info["structured_output"] is True
  85. gpt5_mini_info = get_model_info("gpt-5-mini")
  86. assert gpt5_mini_info["vision"] is True
  87. assert gpt5_mini_info["function_calling"] is True
  88. gpt5_nano_info = get_model_info("gpt-5-nano")
  89. assert gpt5_nano_info["vision"] is True
  90. assert gpt5_nano_info["function_calling"] is True
  91. def test_gpt5_token_limits(self) -> None:
  92. """Test GPT-5 models have correct token limits."""
  93. from autogen_ext.models.openai._model_info import get_token_limit
  94. assert get_token_limit("gpt-5") == 400000
  95. assert get_token_limit("gpt-5-mini") == 400000
  96. assert get_token_limit("gpt-5-nano") == 400000
  97. class TestCustomToolsIntegration:
  98. """Test GPT-5 custom tools functionality."""
  99. def test_custom_tool_schema_generation(self) -> None:
  100. """Test custom tool schema generation."""
  101. code_tool = TestCodeExecutorTool()
  102. schema = code_tool.schema
  103. assert schema["name"] == "code_exec"
  104. assert schema.get("description", "") == "Executes arbitrary Python code and returns the result"
  105. assert "format" not in schema # No grammar constraints
  106. def test_custom_tool_with_grammar_schema(self) -> None:
  107. """Test custom tool with grammar constraints."""
  108. sql_tool = TestSQLTool()
  109. schema = sql_tool.schema
  110. assert schema["name"] == "sql_query"
  111. assert "format" in schema
  112. fmt_any = schema.get("format")
  113. assert isinstance(fmt_any, dict)
  114. assert fmt_any.get("type") == "grammar"
  115. assert fmt_any.get("syntax") == "lark"
  116. assert isinstance(fmt_any.get("definition"), str) and "SELECT" in fmt_any.get("definition", "")
  117. def test_convert_custom_tools(self) -> None:
  118. """Test conversion of custom tools to OpenAI API format."""
  119. code_tool = TestCodeExecutorTool()
  120. sql_tool = TestSQLTool()
  121. converted = convert_tools([code_tool, sql_tool])
  122. assert len(converted) == 2
  123. # Check code tool conversion
  124. code_tool_param = next(
  125. cast(Dict[str, Any], t)
  126. for t in converted
  127. if cast(Dict[str, Any], t).get("custom", {}).get("name") == "code_exec"
  128. )
  129. assert str(code_tool_param.get("type")) == "custom"
  130. assert "format" not in code_tool_param.get("custom", {})
  131. # Check SQL tool conversion with grammar
  132. sql_tool_param = next(
  133. cast(Dict[str, Any], t)
  134. for t in converted
  135. if cast(Dict[str, Any], t).get("custom", {}).get("name") == "sql_query"
  136. )
  137. assert str(sql_tool_param.get("type")) == "custom"
  138. assert "format" in sql_tool_param.get("custom", {})
  139. assert sql_tool_param.get("custom", {}).get("format", {}).get("type") == "grammar"
  140. async def test_custom_tool_execution(self) -> None:
  141. """Test custom tool execution."""
  142. code_tool = TestCodeExecutorTool()
  143. result = await code_tool.run("print('hello world')", CancellationToken())
  144. assert result.result == "Executed: print('hello world')"
  145. result_via_freeform = await code_tool.run_freeform("x = 2 + 2", CancellationToken())
  146. assert result_via_freeform == "Executed: x = 2 + 2"
  147. class TestGPT5Parameters:
  148. """Test GPT-5 specific parameters."""
  149. @pytest.fixture
  150. def mock_openai_client(self) -> Any:
  151. """Mock OpenAI client for testing."""
  152. with patch("autogen_ext.models.openai._openai_client._openai_client_from_config") as mock:
  153. mock_client = AsyncMock()
  154. mock_client.chat.completions.create = AsyncMock()
  155. mock.return_value = mock_client
  156. yield mock_client
  157. @pytest.fixture
  158. def client(self, mock_openai_client: Any) -> OpenAIChatCompletionClient:
  159. """Create test client with mocked OpenAI client."""
  160. return OpenAIChatCompletionClient(model="gpt-5", api_key="test-key")
  161. async def test_reasoning_effort_parameter(
  162. self, client: OpenAIChatCompletionClient, mock_openai_client: Any
  163. ) -> None:
  164. """Test reasoning_effort parameter is properly passed."""
  165. # Mock successful API response
  166. mock_response = ChatCompletion(
  167. id="test-id",
  168. object="chat.completion",
  169. created=1234567890,
  170. model="gpt-5",
  171. choices=[
  172. Choice(
  173. index=0,
  174. message=ChatCompletionMessage(role="assistant", content="Test response"),
  175. finish_reason="stop",
  176. )
  177. ],
  178. usage=CompletionUsage(prompt_tokens=10, completion_tokens=20, total_tokens=30),
  179. )
  180. mock_openai_client.chat.completions.create.return_value = mock_response
  181. # Test different reasoning efforts
  182. for effort in ["minimal", "low", "medium", "high"]:
  183. await client.create(messages=[UserMessage(content="Test message", source="user")], reasoning_effort=effort) # type: ignore[arg-type]
  184. # Verify parameter was passed correctly
  185. call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
  186. assert call_kwargs["reasoning_effort"] == effort
  187. async def test_verbosity_parameter(self, client: OpenAIChatCompletionClient, mock_openai_client: Any) -> None:
  188. """Test verbosity parameter is properly passed."""
  189. mock_response = ChatCompletion(
  190. id="test-id",
  191. object="chat.completion",
  192. created=1234567890,
  193. model="gpt-5",
  194. choices=[
  195. Choice(
  196. index=0,
  197. message=ChatCompletionMessage(role="assistant", content="Test response"),
  198. finish_reason="stop",
  199. )
  200. ],
  201. usage=CompletionUsage(prompt_tokens=10, completion_tokens=20, total_tokens=30),
  202. )
  203. mock_openai_client.chat.completions.create.return_value = mock_response
  204. # Test different verbosity levels
  205. for verbosity in ["low", "medium", "high"]:
  206. await client.create(messages=[UserMessage(content="Test message", source="user")], verbosity=verbosity) # type: ignore[arg-type]
  207. call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
  208. assert call_kwargs["verbosity"] == verbosity
  209. async def test_preambles_parameter(self, client: OpenAIChatCompletionClient, mock_openai_client: Any) -> None:
  210. """Test preambles parameter is properly passed."""
  211. mock_response = ChatCompletion(
  212. id="test-id",
  213. object="chat.completion",
  214. created=1234567890,
  215. model="gpt-5",
  216. choices=[
  217. Choice(
  218. index=0,
  219. message=ChatCompletionMessage(role="assistant", content="Test response"),
  220. finish_reason="stop",
  221. )
  222. ],
  223. usage=CompletionUsage(prompt_tokens=10, completion_tokens=20, total_tokens=30),
  224. )
  225. mock_openai_client.chat.completions.create.return_value = mock_response
  226. # Test preambles enabled
  227. await client.create(messages=[UserMessage(content="Test message", source="user")], preambles=True)
  228. call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
  229. assert call_kwargs["preambles"] is True
  230. # Test preambles disabled
  231. await client.create(messages=[UserMessage(content="Test message", source="user")], preambles=False)
  232. call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
  233. assert call_kwargs["preambles"] is False
  234. async def test_combined_gpt5_parameters(self, client: OpenAIChatCompletionClient, mock_openai_client: Any) -> None:
  235. """Test multiple GPT-5 parameters used together."""
  236. mock_response = ChatCompletion(
  237. id="test-id",
  238. object="chat.completion",
  239. created=1234567890,
  240. model="gpt-5",
  241. choices=[
  242. Choice(
  243. index=0,
  244. message=ChatCompletionMessage(role="assistant", content="Test response"),
  245. finish_reason="stop",
  246. )
  247. ],
  248. usage=CompletionUsage(prompt_tokens=10, completion_tokens=20, total_tokens=30),
  249. )
  250. mock_openai_client.chat.completions.create.return_value = mock_response
  251. await client.create(
  252. messages=[UserMessage(content="Test message", source="user")],
  253. reasoning_effort="high",
  254. verbosity="medium",
  255. preambles=True,
  256. )
  257. call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
  258. assert call_kwargs["reasoning_effort"] == "high"
  259. assert call_kwargs["verbosity"] == "medium"
  260. assert call_kwargs["preambles"] is True
  261. class TestAllowedToolsFeature:
  262. """Test GPT-5 allowed_tools parameter for restricting tool usage."""
  263. @pytest.fixture
  264. def mock_openai_client(self) -> Any:
  265. with patch("autogen_ext.models.openai._openai_client._openai_client_from_config") as mock:
  266. mock_client = AsyncMock()
  267. mock_client.chat.completions.create = AsyncMock()
  268. mock.return_value = mock_client
  269. yield mock_client
  270. @pytest.fixture
  271. def client(self, mock_openai_client: Any) -> OpenAIChatCompletionClient:
  272. return OpenAIChatCompletionClient(model="gpt-5", api_key="test-key")
  273. async def test_allowed_tools_restriction(self, client: OpenAIChatCompletionClient, mock_openai_client: Any) -> None:
  274. """Test allowed_tools parameter restricts model to specific tools."""
  275. from autogen_core.tools import FunctionTool
  276. def safe_calc(x: int, y: int) -> int:
  277. return x + y
  278. def dangerous_exec(code: str) -> str:
  279. return f"Would execute: {code}"
  280. calc_tool = FunctionTool(safe_calc, description="Safe calculator")
  281. exec_tool = FunctionTool(dangerous_exec, description="Code executor")
  282. code_tool = TestCodeExecutorTool()
  283. from autogen_core.tools import CustomTool as _CustomTool
  284. from autogen_core.tools import CustomToolSchema as _CustomToolSchema
  285. from autogen_core.tools import Tool as _Tool
  286. from autogen_core.tools import ToolSchema as _ToolSchema
  287. all_tools: List[_Tool | _ToolSchema | _CustomTool | _CustomToolSchema] = [
  288. cast(_Tool, calc_tool),
  289. cast(_Tool, exec_tool),
  290. cast(_CustomTool, code_tool),
  291. ]
  292. safe_tools: List[_Tool | _CustomTool | str] = [cast(_Tool, calc_tool)] # Only allow calculator
  293. mock_response = ChatCompletion(
  294. id="test-id",
  295. object="chat.completion",
  296. created=1234567890,
  297. model="gpt-5",
  298. choices=[
  299. Choice(
  300. index=0,
  301. message=ChatCompletionMessage(role="assistant", content="Test response"),
  302. finish_reason="stop",
  303. )
  304. ],
  305. usage=CompletionUsage(prompt_tokens=10, completion_tokens=20, total_tokens=30),
  306. )
  307. mock_openai_client.chat.completions.create.return_value = mock_response
  308. await client.create(
  309. messages=[UserMessage(content="Help with math and coding", source="user")],
  310. tools=all_tools,
  311. allowed_tools=safe_tools,
  312. tool_choice="auto",
  313. )
  314. call_kwargs_any: Any = mock_openai_client.chat.completions.create.call_args[1]
  315. # Verify allowed_tools structure was created
  316. call_kwargs: Dict[str, Any] = cast(Dict[str, Any], call_kwargs_any)
  317. assert "tool_choice" in call_kwargs
  318. tool_choice_val: Any = call_kwargs.get("tool_choice")
  319. if isinstance(tool_choice_val, dict):
  320. tc: Dict[str, Any] = cast(Dict[str, Any], tool_choice_val)
  321. if str(tc.get("type", "")) == "allowed_tools":
  322. mode_val: str = str(tc.get("mode", ""))
  323. assert mode_val == "auto"
  324. tools_seq: List[Any] = list(cast(List[Any] | tuple[Any, ...], tc.get("tools", [])))
  325. tools_list: List[Dict[str, Any]] = [t for t in tools_seq if isinstance(t, dict)]
  326. allowed_tool_names: List[str] = [str(t.get("name", "")) for t in tools_list]
  327. assert "safe_calc" in allowed_tool_names
  328. assert "dangerous_exec" not in allowed_tool_names
  329. assert "code_exec" not in allowed_tool_names
  330. class TestResponsesAPIClient:
  331. """Test the dedicated Responses API client for GPT-5."""
  332. @pytest.fixture
  333. def mock_openai_client(self) -> Any:
  334. with patch("autogen_ext.models.openai._responses_client._openai_client_from_config") as mock:
  335. mock_client = AsyncMock()
  336. mock_client.responses.create = AsyncMock()
  337. mock.return_value = mock_client
  338. yield mock_client
  339. @pytest.fixture
  340. def responses_client(self, mock_openai_client: Any) -> OpenAIResponsesAPIClient:
  341. return OpenAIResponsesAPIClient(model="gpt-5", api_key="test-key")
  342. async def test_responses_api_basic_call(
  343. self, responses_client: OpenAIResponsesAPIClient, mock_openai_client: Any
  344. ) -> None:
  345. """Test basic Responses API call structure."""
  346. mock_response = {
  347. "id": "resp-123",
  348. "choices": [{"message": {"content": "Response content"}, "finish_reason": "stop"}],
  349. "usage": {"prompt_tokens": 10, "completion_tokens": 20},
  350. }
  351. mock_openai_client.responses.create.return_value = mock_response
  352. result = await responses_client.create(input="Test input message", reasoning_effort="medium", verbosity="high")
  353. assert isinstance(result, CreateResult)
  354. assert result.content == "Response content"
  355. assert result.usage.prompt_tokens == 10
  356. assert result.usage.completion_tokens == 20
  357. async def test_responses_api_with_cot_preservation(
  358. self, responses_client: OpenAIResponsesAPIClient, mock_openai_client: Any
  359. ) -> None:
  360. """Test chain-of-thought preservation between turns."""
  361. # First turn
  362. mock_response1 = {
  363. "id": "resp-123",
  364. "choices": [{"message": {"content": "First response"}, "finish_reason": "stop"}],
  365. "usage": {"prompt_tokens": 10, "completion_tokens": 20},
  366. "reasoning_items": [{"type": "reasoning", "content": "Initial reasoning"}],
  367. }
  368. mock_openai_client.responses.create.return_value = mock_response1
  369. result1 = await responses_client.create(input="First question", reasoning_effort="high")
  370. # Second turn with preserved CoT
  371. mock_response2 = {
  372. "id": "resp-124",
  373. "choices": [{"message": {"content": "Follow-up response"}, "finish_reason": "stop"}],
  374. "usage": {"prompt_tokens": 5, "completion_tokens": 15}, # Lower usage due to CoT reuse
  375. }
  376. mock_openai_client.responses.create.return_value = mock_response2
  377. result2 = await responses_client.create(
  378. input="Follow-up question",
  379. previous_response_id=result1.response_id, # type: ignore
  380. reasoning_effort="low", # Can use lower effort
  381. )
  382. # Verify previous_response_id was passed
  383. call_kwargs = mock_openai_client.responses.create.call_args[1]
  384. assert call_kwargs["previous_response_id"] == "resp-123"
  385. assert call_kwargs["reasoning"]["effort"] == "low"
  386. assert result2.content == "Follow-up response"
  387. async def test_responses_api_with_custom_tools(
  388. self, responses_client: OpenAIResponsesAPIClient, mock_openai_client: Any
  389. ) -> None:
  390. """Test Responses API with GPT-5 custom tools."""
  391. code_tool = TestCodeExecutorTool()
  392. mock_response = {
  393. "id": "resp-125",
  394. "choices": [
  395. {
  396. "message": {
  397. "content": "I'll execute the code for you.",
  398. "tool_calls": [
  399. {"id": "call-456", "custom": {"name": "code_exec", "input": "print('Hello GPT-5')"}}
  400. ],
  401. },
  402. "finish_reason": "tool_calls",
  403. }
  404. ],
  405. "usage": {"prompt_tokens": 15, "completion_tokens": 25},
  406. }
  407. mock_openai_client.responses.create.return_value = mock_response
  408. result = await responses_client.create(
  409. input="Run this Python code: print('Hello GPT-5')", tools=[code_tool], preambles=True
  410. )
  411. assert isinstance(result.content, list)
  412. assert len(result.content) == 1
  413. assert result.content[0].name == "code_exec"
  414. assert result.content[0].arguments == "print('Hello GPT-5')"
  415. assert result.thought == "I'll execute the code for you." # Preamble text
  416. class TestGPT5IntegrationScenarios:
  417. """Test realistic GPT-5 usage scenarios."""
  418. @pytest.fixture
  419. def mock_openai_client(self) -> Any:
  420. with patch("autogen_ext.models.openai._openai_client._openai_client_from_config") as mock:
  421. mock_client = AsyncMock()
  422. mock_client.chat.completions.create = AsyncMock()
  423. mock.return_value = mock_client
  424. yield mock_client
  425. @pytest.fixture
  426. def client(self, mock_openai_client: Any) -> OpenAIChatCompletionClient:
  427. return OpenAIChatCompletionClient(model="gpt-5", api_key="test-key")
  428. async def test_code_analysis_with_custom_tools(
  429. self, client: OpenAIChatCompletionClient, mock_openai_client: Any
  430. ) -> None:
  431. """Test GPT-5 analyzing and executing code with custom tools."""
  432. code_tool = TestCodeExecutorTool()
  433. sql_tool = TestSQLTool()
  434. mock_response = ChatCompletion(
  435. id="test-id",
  436. object="chat.completion",
  437. created=1234567890,
  438. model="gpt-5",
  439. choices=[
  440. Choice(
  441. index=0,
  442. message=ChatCompletionMessage(
  443. role="assistant",
  444. content="I need to analyze this code and run it.",
  445. tool_calls=[
  446. ChatCompletionMessageToolCall(
  447. id="call-123",
  448. type="custom", # type: ignore
  449. custom={ # type: ignore
  450. "name": "code_exec",
  451. "input": "def fibonacci(n):\n return n if n <= 1 else fibonacci(n-1) + fibonacci(n-2)\nprint(fibonacci(10))",
  452. },
  453. )
  454. ],
  455. ),
  456. finish_reason="tool_calls",
  457. )
  458. ],
  459. usage=CompletionUsage(prompt_tokens=50, completion_tokens=30, total_tokens=80),
  460. )
  461. mock_openai_client.chat.completions.create.return_value = mock_response
  462. # Tools typed to expected union for create
  463. tools_param = [code_tool, sql_tool]
  464. result = await client.create(
  465. messages=[UserMessage(content="Analyze this fibonacci implementation and run it for n=10", source="user")],
  466. tools=tools_param,
  467. reasoning_effort="medium", # type: ignore[arg-type]
  468. verbosity="low", # type: ignore[arg-type]
  469. preambles=True,
  470. )
  471. # Verify GPT-5 parameters were passed
  472. call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
  473. assert call_kwargs["reasoning_effort"] == "medium"
  474. assert call_kwargs["verbosity"] == "low"
  475. assert call_kwargs["preambles"] is True
  476. # Verify tools were converted properly
  477. assert "tools" in call_kwargs
  478. tools = call_kwargs["tools"]
  479. assert len(tools) == 2
  480. # Check that result contains tool call
  481. assert isinstance(result.content, list)
  482. assert len(result.content) == 1
  483. assert result.thought == "I need to analyze this code and run it."
  484. async def test_multi_modal_with_reasoning_control(
  485. self, client: OpenAIChatCompletionClient, mock_openai_client: Any
  486. ) -> None:
  487. """Test GPT-5 with vision and reasoning control."""
  488. import io
  489. from autogen_core import Image
  490. from PIL import Image as PILImage
  491. # Create a simple test image
  492. pil_image = PILImage.new("RGB", (100, 100), color="red")
  493. image_bytes = io.BytesIO()
  494. pil_image.save(image_bytes, format="PNG")
  495. image_bytes.seek(0)
  496. test_image = Image.from_pil(pil_image)
  497. mock_response = ChatCompletion(
  498. id="test-id",
  499. object="chat.completion",
  500. created=1234567890,
  501. model="gpt-5",
  502. choices=[
  503. Choice(
  504. index=0,
  505. message=ChatCompletionMessage(
  506. role="assistant", content="I can see this is a red square image. Let me analyze it further..."
  507. ),
  508. finish_reason="stop",
  509. )
  510. ],
  511. usage=CompletionUsage(prompt_tokens=100, completion_tokens=40, total_tokens=140),
  512. )
  513. mock_openai_client.chat.completions.create.return_value = mock_response
  514. result = await client.create(
  515. messages=[UserMessage(content=["What do you see in this image?", test_image], source="user")],
  516. reasoning_effort="high",
  517. verbosity="high",
  518. )
  519. assert result.content == "I can see this is a red square image. Let me analyze it further..."
  520. # Verify vision-related processing occurred
  521. call_kwargs = mock_openai_client.chat.completions.create.call_args[1]
  522. assert call_kwargs["reasoning_effort"] == "high"
  523. assert call_kwargs["verbosity"] == "high"
  524. @pytest.mark.asyncio
  525. async def test_gpt5_error_handling() -> None:
  526. """Test proper error handling for GPT-5 specific scenarios."""
  527. # Client should construct without error
  528. _ = OpenAIChatCompletionClient(model="gpt-5", api_key="test-key")
  529. # Test model without GPT-5 capabilities using GPT-5 features
  530. with patch("autogen_ext.models.openai._openai_client._openai_client_from_config") as mock:
  531. mock_client = AsyncMock()
  532. mock.return_value = mock_client
  533. # Test with non-GPT-5 model
  534. old_model_client = OpenAIChatCompletionClient(model="gpt-4", api_key="test-key")
  535. # GPT-4 should still accept these parameters (they'll be ignored by the API)
  536. mock_client.chat.completions.create.return_value = ChatCompletion(
  537. id="test",
  538. object="chat.completion",
  539. created=1234567890,
  540. model="gpt-4",
  541. choices=[],
  542. usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
  543. )
  544. # This should work but parameters won't have any effect
  545. await old_model_client.create(
  546. messages=[UserMessage(content="Test", source="user")],
  547. reasoning_effort="high", # Will be passed but ignored
  548. preambles=True,
  549. )
  550. if __name__ == "__main__":
  551. # Run basic validation tests
  552. pytest.main([__file__, "-v"])