|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | 3 | import asyncio |
| 4 | +import copy |
4 | 5 | import json |
5 | 6 | import logging |
6 | 7 | from types import SimpleNamespace |
|
13 | 14 | from openai.types.responses.response import IncompleteDetails |
14 | 15 | from openai.types.responses.response_create_params import ContextManagement, PromptCacheOptions |
15 | 16 | from openai.types.responses.response_usage import ResponseUsage |
| 17 | +from openai.types.responses.tool_param import ImageGeneration |
16 | 18 | from openai.types.shared.reasoning import Reasoning |
17 | 19 |
|
18 | 20 | from agents import ( |
19 | 21 | Agent, |
20 | 22 | AsyncComputer, |
21 | 23 | Computer, |
22 | 24 | ComputerTool, |
| 25 | + ImageGenerationTool, |
23 | 26 | ModelSettings, |
24 | 27 | ModelTracing, |
25 | 28 | Runner, |
@@ -249,6 +252,130 @@ async def handler(request: httpx2.Request) -> httpx2.Response: |
249 | 252 | assert body.get("include", []) == expected_include |
250 | 253 |
|
251 | 254 |
|
| 255 | +@pytest.mark.allow_call_model_methods |
| 256 | +@pytest.mark.asyncio |
| 257 | +@pytest.mark.parametrize("stream", [False, True], ids=["non_streaming", "streaming"]) |
| 258 | +@pytest.mark.parametrize( |
| 259 | + "tool", |
| 260 | + [ |
| 261 | + ImageGenerationTool(ImageGeneration(type="image_generation")), |
| 262 | + ImageGenerationTool( |
| 263 | + ImageGeneration( |
| 264 | + type="image_generation", |
| 265 | + model="gpt-image-1", |
| 266 | + quality="high", |
| 267 | + action="edit", |
| 268 | + input_fidelity="high", |
| 269 | + input_image_mask={"file_id": "file-mask"}, |
| 270 | + ) |
| 271 | + ), |
| 272 | + ImageGenerationTool( |
| 273 | + tool_config={ |
| 274 | + "type": "image_generation", |
| 275 | + "model": "gpt-image-2.5-sunburst", |
| 276 | + "quality": "max", |
| 277 | + "action": "generate", |
| 278 | + "size": "1536x864", |
| 279 | + "background": "transparent", |
| 280 | + "output_format": "webp", |
| 281 | + "output_compression": 0, |
| 282 | + "partial_images": 0, |
| 283 | + "moderation": "auto", |
| 284 | + } |
| 285 | + ), |
| 286 | + ImageGenerationTool( |
| 287 | + tool_config={ |
| 288 | + "type": "image_generation", |
| 289 | + "model": "gpt-image-2.5-flare", |
| 290 | + "quality": "xhigh", |
| 291 | + "action": "auto", |
| 292 | + "size": "auto", |
| 293 | + "output_format": "png", |
| 294 | + "partial_images": 2, |
| 295 | + } |
| 296 | + ), |
| 297 | + ], |
| 298 | + ids=["defaults", "typed_edit", "sunburst_max", "flare_xhigh"], |
| 299 | +) |
| 300 | +async def test_image_generation_options_reach_responses_request( |
| 301 | + stream: bool, tool: ImageGenerationTool |
| 302 | +) -> None: |
| 303 | + """Use the real client's serializer to check options beyond its current annotations.""" |
| 304 | + expected_config = copy.deepcopy(tool.tool_config) |
| 305 | + request_bodies: list[dict[str, Any]] = [] |
| 306 | + |
| 307 | + async def handler(request: httpx2.Request) -> httpx2.Response: |
| 308 | + request_bodies.append(json.loads(request.content)) |
| 309 | + if stream: |
| 310 | + event = _response_completed_frame("resp-id", sequence_number=0) |
| 311 | + return httpx2.Response( |
| 312 | + 200, |
| 313 | + content=f"event: response.completed\ndata: {event}\n\n", |
| 314 | + headers={"content-type": "text/event-stream"}, |
| 315 | + ) |
| 316 | + return httpx2.Response( |
| 317 | + 200, |
| 318 | + content=get_response_obj([]).model_dump_json(), |
| 319 | + headers={"content-type": "application/json"}, |
| 320 | + ) |
| 321 | + |
| 322 | + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as http_client: |
| 323 | + agent = Agent( |
| 324 | + name="Image generator", |
| 325 | + model=OpenAIResponsesModel( |
| 326 | + model="gpt-5.5", |
| 327 | + openai_client=AsyncOpenAI(api_key="test-key", http_client=http_client), |
| 328 | + ), |
| 329 | + tools=[tool], |
| 330 | + ) |
| 331 | + prompt = [ |
| 332 | + { |
| 333 | + "role": "user", |
| 334 | + "content": [ |
| 335 | + {"type": "input_text", "text": "Generate an image, or edit this image."}, |
| 336 | + {"type": "input_image", "file_id": "file-image", "detail": "auto"}, |
| 337 | + ], |
| 338 | + } |
| 339 | + ] |
| 340 | + if stream: |
| 341 | + result = Runner.run_streamed(agent, prompt) |
| 342 | + async for _ in result.stream_events(): |
| 343 | + pass |
| 344 | + else: |
| 345 | + await Runner.run(agent, prompt) |
| 346 | + |
| 347 | + assert len(request_bodies) == 1 |
| 348 | + assert request_bodies[0]["tools"] == [expected_config] |
| 349 | + assert tool.tool_config == expected_config |
| 350 | + assert request_bodies[0]["model"] == "gpt-5.5" |
| 351 | + |
| 352 | + |
| 353 | +def test_image_generation_config_remains_mutable() -> None: |
| 354 | + config: ImageGeneration = {"type": "image_generation", "quality": "low"} |
| 355 | + tool = ImageGenerationTool(config) |
| 356 | + assert tool.tool_config is config |
| 357 | + tool.tool_config["quality"] = "high" |
| 358 | + assert config["quality"] == "high" |
| 359 | + |
| 360 | + replacement: ImageGeneration = {"type": "image_generation", "quality": "auto"} |
| 361 | + tool.tool_config = replacement |
| 362 | + assert tool.tool_config is replacement |
| 363 | + |
| 364 | + tool.tool_config = { |
| 365 | + "type": "image_generation", |
| 366 | + "model": "gpt-image-2.5-flare", |
| 367 | + "quality": "xhigh", |
| 368 | + } |
| 369 | + tool.tool_config["quality"] = "max" |
| 370 | + assert Converter.convert_tools([tool], []).tools == [ |
| 371 | + { |
| 372 | + "type": "image_generation", |
| 373 | + "model": "gpt-image-2.5-flare", |
| 374 | + "quality": "max", |
| 375 | + } |
| 376 | + ] |
| 377 | + |
| 378 | + |
252 | 379 | class DummyWSConnection: |
253 | 380 | def __init__(self, frames: list[str]): |
254 | 381 | self._frames = frames |
|
0 commit comments