Skip to content

Commit 21da115

Browse files
authored
fix: #2636 make Computer preview metadata optional on GA requests (#2639)
1 parent fc6afeb commit 21da115

File tree

3 files changed

+161
-16
lines changed

3 files changed

+161
-16
lines changed

src/agents/computer.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ class Computer(abc.ABC):
1010
operations needed to control a computer or browser."""
1111

1212
@property
13-
@abc.abstractmethod
14-
def environment(self) -> Environment:
15-
pass
13+
def environment(self) -> Environment | None:
14+
"""Return preview tool metadata when the preview computer payload is required."""
15+
return None
1616

1717
@property
18-
@abc.abstractmethod
19-
def dimensions(self) -> tuple[int, int]:
20-
pass
18+
def dimensions(self) -> tuple[int, int] | None:
19+
"""Return preview display dimensions when the preview computer payload is required."""
20+
return None
2121

2222
@abc.abstractmethod
2323
def screenshot(self) -> str:
@@ -61,14 +61,14 @@ class AsyncComputer(abc.ABC):
6161
operations needed to control a computer or browser."""
6262

6363
@property
64-
@abc.abstractmethod
65-
def environment(self) -> Environment:
66-
pass
64+
def environment(self) -> Environment | None:
65+
"""Return preview tool metadata when the preview computer payload is required."""
66+
return None
6767

6868
@property
69-
@abc.abstractmethod
70-
def dimensions(self) -> tuple[int, int]:
71-
pass
69+
def dimensions(self) -> tuple[int, int] | None:
70+
"""Return preview display dimensions when the preview computer payload is required."""
71+
return None
7272

7373
@abc.abstractmethod
7474
async def screenshot(self) -> str:

src/agents/models/openai_responses.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1768,12 +1768,19 @@ def _convert_preview_computer_tool(cls, tool: ComputerTool[Any]) -> ResponsesToo
17681768
"resolve_computer({ tool, run_context }) with a run context first "
17691769
"when building payloads manually."
17701770
)
1771+
environment = computer.environment
1772+
dimensions = computer.dimensions
1773+
if environment is None or dimensions is None:
1774+
raise UserError(
1775+
"Preview computer tool payloads require `environment` and `dimensions` on the "
1776+
"Computer/AsyncComputer implementation."
1777+
)
17711778
return _require_responses_tool_param(
17721779
{
17731780
"type": "computer_use_preview",
1774-
"environment": computer.environment,
1775-
"display_width": computer.dimensions[0],
1776-
"display_height": computer.dimensions[1],
1781+
"environment": environment,
1782+
"display_width": dimensions[0],
1783+
"display_height": dimensions[1],
17771784
}
17781785
)
17791786

tests/test_openai_responses.py

Lines changed: 139 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,15 @@
1111
from openai.types.responses import ResponseCompletedEvent
1212
from openai.types.shared.reasoning import Reasoning
1313

14-
from agents import Computer, ComputerTool, ModelSettings, ModelTracing, ToolSearchTool, __version__
14+
from agents import (
15+
AsyncComputer,
16+
Computer,
17+
ComputerTool,
18+
ModelSettings,
19+
ModelTracing,
20+
ToolSearchTool,
21+
__version__,
22+
)
1523
from agents.exceptions import UserError
1624
from agents.models.openai_responses import (
1725
_HEADERS_OVERRIDE as RESP_HEADERS,
@@ -932,6 +940,69 @@ def __init__(self):
932940
assert called_kwargs["tools"] == [{"type": "tool_search"}]
933941

934942

943+
@pytest.mark.allow_call_model_methods
944+
@pytest.mark.asyncio
945+
async def test_ga_computer_tool_does_not_require_preview_metadata() -> None:
946+
called_kwargs: dict[str, Any] = {}
947+
948+
class DummyComputer(AsyncComputer):
949+
async def screenshot(self) -> str:
950+
return "screenshot"
951+
952+
async def click(self, x: int, y: int, button: str) -> None:
953+
pass
954+
955+
async def double_click(self, x: int, y: int) -> None:
956+
pass
957+
958+
async def drag(self, path: list[tuple[int, int]]) -> None:
959+
pass
960+
961+
async def keypress(self, keys: list[str]) -> None:
962+
pass
963+
964+
async def move(self, x: int, y: int) -> None:
965+
pass
966+
967+
async def scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
968+
pass
969+
970+
async def type(self, text: str) -> None:
971+
pass
972+
973+
async def wait(self) -> None:
974+
pass
975+
976+
class DummyResponses:
977+
async def create(self, **kwargs):
978+
nonlocal called_kwargs
979+
called_kwargs = kwargs
980+
return get_response_obj([])
981+
982+
class DummyResponsesClient:
983+
def __init__(self):
984+
self.responses = DummyResponses()
985+
986+
model = OpenAIResponsesModel(
987+
model="gpt-5.4",
988+
openai_client=DummyResponsesClient(), # type: ignore[arg-type]
989+
model_is_explicit=True,
990+
)
991+
992+
await model.get_response(
993+
system_instructions=None,
994+
input="hi",
995+
model_settings=ModelSettings(),
996+
tools=[ComputerTool(computer=DummyComputer())],
997+
output_schema=None,
998+
handoffs=[],
999+
tracing=ModelTracing.DISABLED,
1000+
prompt=None,
1001+
)
1002+
1003+
assert called_kwargs["tools"] == [{"type": "computer"}]
1004+
1005+
9351006
@pytest.mark.allow_call_model_methods
9361007
@pytest.mark.asyncio
9371008
async def test_prompt_id_uses_preview_computer_payload_when_prompt_owns_model() -> None:
@@ -1012,6 +1083,73 @@ def __init__(self):
10121083
]
10131084

10141085

1086+
@pytest.mark.allow_call_model_methods
1087+
@pytest.mark.asyncio
1088+
async def test_prompt_id_computer_without_preview_metadata_raises_clear_error() -> None:
1089+
called_kwargs: dict[str, Any] = {}
1090+
1091+
class DummyComputer(Computer):
1092+
def screenshot(self) -> str:
1093+
return "screenshot"
1094+
1095+
def click(self, x: int, y: int, button: str) -> None:
1096+
pass
1097+
1098+
def double_click(self, x: int, y: int) -> None:
1099+
pass
1100+
1101+
def drag(self, path: list[tuple[int, int]]) -> None:
1102+
pass
1103+
1104+
def keypress(self, keys: list[str]) -> None:
1105+
pass
1106+
1107+
def move(self, x: int, y: int) -> None:
1108+
pass
1109+
1110+
def scroll(self, x: int, y: int, scroll_x: int, scroll_y: int) -> None:
1111+
pass
1112+
1113+
def type(self, text: str) -> None:
1114+
pass
1115+
1116+
def wait(self) -> None:
1117+
pass
1118+
1119+
class DummyResponses:
1120+
async def create(self, **kwargs):
1121+
nonlocal called_kwargs
1122+
called_kwargs = kwargs
1123+
return get_response_obj([])
1124+
1125+
class DummyResponsesClient:
1126+
def __init__(self):
1127+
self.responses = DummyResponses()
1128+
1129+
model = OpenAIResponsesModel(
1130+
model="gpt-5.4",
1131+
openai_client=DummyResponsesClient(), # type: ignore[arg-type]
1132+
model_is_explicit=False,
1133+
)
1134+
1135+
with pytest.raises(
1136+
UserError,
1137+
match="Preview computer tool payloads require `environment` and `dimensions`",
1138+
):
1139+
await model.get_response(
1140+
system_instructions=None,
1141+
input="hi",
1142+
model_settings=ModelSettings(),
1143+
tools=[ComputerTool(computer=DummyComputer())],
1144+
output_schema=None,
1145+
handoffs=[],
1146+
tracing=ModelTracing.DISABLED,
1147+
prompt={"id": "pmpt_123"},
1148+
)
1149+
1150+
assert called_kwargs == {}
1151+
1152+
10151153
@pytest.mark.allow_call_model_methods
10161154
@pytest.mark.asyncio
10171155
async def test_prompt_id_unresolved_computer_uses_preview_payload_shape() -> None:

0 commit comments

Comments
 (0)