Skip to content

Commit 4f40c01

Browse files
authored
fix: #2624 migrate ComputerTool to the GA computer tool (#2626)
1 parent 0f9a370 commit 4f40c01

16 files changed

+929
-88
lines changed

examples/tools/computer_use.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
ComputerProvider,
1717
ComputerTool,
1818
Environment,
19-
ModelSettings,
2019
RunContextWrapper,
2120
Runner,
2221
trace,
@@ -174,9 +173,8 @@ async def run_agent(
174173
name="Browser user",
175174
instructions="You are a helpful agent. Find the current weather in Tokyo.",
176175
tools=[ComputerTool(computer=computer_config)],
177-
# Use the computer using model, and set truncation to auto because it is required.
178-
model="computer-use-preview",
179-
model_settings=ModelSettings(truncation="auto"),
176+
# GPT-5.4 uses the built-in Responses API computer tool.
177+
model="gpt-5.4",
180178
)
181179
result = await Runner.run(agent, "What is the weather in Tokyo right now?")
182180
print(result.final_output)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ requires-python = ">=3.10"
77
license = "MIT"
88
authors = [{ name = "OpenAI", email = "support@openai.com" }]
99
dependencies = [
10-
"openai>=2.25.0,<3",
10+
"openai>=2.26.0,<3",
1111
"pydantic>=2.12.2, <3",
1212
"griffe>=1.5.6, <2",
1313
"typing-extensions>=4.12.2, <5",

src/agents/models/openai_responses.py

Lines changed: 138 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from openai.types import ChatModel
1717
from openai.types.responses import (
1818
ApplyPatchToolParam,
19-
ComputerToolParam,
2019
FileSearchToolParam,
2120
FunctionToolParam,
2221
Response,
@@ -628,23 +627,38 @@ def _build_response_create_kwargs(
628627
else:
629628
parallel_tool_calls = omit
630629

630+
should_omit_model = prompt is not None and not self._model_is_explicit
631+
effective_request_model: str | ChatModel | None = None if should_omit_model else self.model
632+
effective_computer_tool_model = Converter.resolve_computer_tool_model(
633+
request_model=effective_request_model,
634+
tools=tools,
635+
)
631636
tool_choice = Converter.convert_tool_choice(
632637
model_settings.tool_choice,
633638
tools=tools,
634639
handoffs=handoffs,
640+
model=effective_computer_tool_model,
635641
)
636642
if prompt is None:
637-
converted_tools = Converter.convert_tools(tools, handoffs)
643+
converted_tools = Converter.convert_tools(
644+
tools,
645+
handoffs,
646+
model=effective_computer_tool_model,
647+
tool_choice=model_settings.tool_choice,
648+
)
638649
else:
639650
converted_tools = Converter.convert_tools(
640651
tools,
641652
handoffs,
642653
allow_opaque_tool_search_surface=True,
654+
model=effective_computer_tool_model,
655+
tool_choice=model_settings.tool_choice,
643656
)
644657
converted_tools_payload = _materialize_responses_tool_params(converted_tools.tools)
645658
response_format = Converter.get_response_format(output_schema)
646-
should_omit_model = prompt is not None and not self._model_is_explicit
647-
model_param: str | ChatModel | Omit = self.model if not should_omit_model else omit
659+
model_param: str | ChatModel | Omit = (
660+
effective_request_model if effective_request_model is not None else omit
661+
)
648662
should_omit_tools = prompt is not None and len(converted_tools_payload) == 0
649663
# In prompt-managed tool flows without local tools payload, omit only named tool choices
650664
# that must match an explicit tool list. Keep control literals like "none"/"required".
@@ -1390,6 +1404,7 @@ def convert_tool_choice(
13901404
*,
13911405
tools: Sequence[Tool] | None = None,
13921406
handoffs: Sequence[Handoff[Any, Any]] | None = None,
1407+
model: str | ChatModel | None = None,
13931408
) -> response_create_params.ToolChoice | Omit:
13941409
if tool_choice is None:
13951410
return omit
@@ -1419,6 +1434,15 @@ def convert_tool_choice(
14191434
return {
14201435
"type": "web_search_preview",
14211436
}
1437+
elif tool_choice in {
1438+
"computer",
1439+
"computer_use",
1440+
"computer_use_preview",
1441+
} and cls._has_computer_tool(tools):
1442+
return cls._convert_builtin_computer_tool_choice(
1443+
tool_choice=tool_choice,
1444+
model=model,
1445+
)
14221446
elif tool_choice == "computer_use_preview":
14231447
return {
14241448
"type": "computer_use_preview",
@@ -1543,6 +1567,79 @@ def _validate_named_function_tool_choice(
15431567
"the tool via ToolSearchTool() first."
15441568
)
15451569

1570+
@classmethod
1571+
def _has_computer_tool(cls, tools: Sequence[Tool] | None) -> bool:
1572+
return any(isinstance(tool, ComputerTool) for tool in tools or ())
1573+
1574+
@classmethod
1575+
def _has_unresolved_computer_tool(cls, tools: Sequence[Tool] | None) -> bool:
1576+
return any(
1577+
isinstance(tool, ComputerTool)
1578+
and not isinstance(tool.computer, (Computer, AsyncComputer))
1579+
for tool in tools or ()
1580+
)
1581+
1582+
@classmethod
1583+
def _is_preview_computer_model(cls, model: str | ChatModel | None) -> bool:
1584+
return isinstance(model, str) and model.startswith("computer-use-preview")
1585+
1586+
@classmethod
1587+
def _is_ga_computer_model(cls, model: str | ChatModel | None) -> bool:
1588+
return isinstance(model, str) and model.startswith("gpt-5.4")
1589+
1590+
@classmethod
1591+
def resolve_computer_tool_model(
1592+
cls,
1593+
*,
1594+
request_model: str | ChatModel | None,
1595+
tools: Sequence[Tool] | None,
1596+
) -> str | ChatModel | None:
1597+
if not cls._has_computer_tool(tools):
1598+
return None
1599+
return request_model
1600+
1601+
@classmethod
1602+
def _should_use_preview_computer_tool(
1603+
cls,
1604+
*,
1605+
model: str | ChatModel | None,
1606+
tool_choice: Literal["auto", "required", "none"] | str | MCPToolChoice | None,
1607+
) -> bool:
1608+
# Choose the computer tool wire shape from the effective request model when we know it.
1609+
# For prompt-managed calls that omit `model`, default to the released preview payload
1610+
# unless the caller explicitly opts into a GA computer-tool selector. The prompt may pin
1611+
# a different model than the local default, so we must not infer the wire shape from
1612+
# `self.model` when the request payload itself omits `model`.
1613+
if cls._is_preview_computer_model(model):
1614+
return True
1615+
if model is not None:
1616+
return False
1617+
if isinstance(tool_choice, str) and tool_choice in {"computer", "computer_use"}:
1618+
return False
1619+
return True
1620+
1621+
@classmethod
1622+
def _convert_builtin_computer_tool_choice(
1623+
cls,
1624+
*,
1625+
tool_choice: Literal["auto", "required", "none"] | str | MCPToolChoice | None,
1626+
model: str | ChatModel | None,
1627+
) -> response_create_params.ToolChoice:
1628+
# Preview models only support the preview computer tool selector, even if callers force
1629+
# a GA-era alias such as "computer" or "computer_use".
1630+
if cls._is_preview_computer_model(model):
1631+
return {
1632+
"type": "computer_use_preview",
1633+
}
1634+
if cls._should_use_preview_computer_tool(model=model, tool_choice=tool_choice):
1635+
return {
1636+
"type": "computer_use_preview",
1637+
}
1638+
# `computer_use` is a compatibility alias, but the GA built-in tool surface is `computer`.
1639+
return {
1640+
"type": "computer",
1641+
}
1642+
15461643
@classmethod
15471644
def get_response_format(
15481645
cls, output_schema: AgentOutputSchemaBase | None
@@ -1566,12 +1663,18 @@ def convert_tools(
15661663
handoffs: list[Handoff[Any, Any]],
15671664
*,
15681665
allow_opaque_tool_search_surface: bool = False,
1666+
model: str | ChatModel | None = None,
1667+
tool_choice: Literal["auto", "required", "none"] | str | MCPToolChoice | None = None,
15691668
) -> ConvertedTools:
15701669
converted_tools: list[ResponsesToolParam | None] = []
15711670
includes: list[ResponseIncludable] = []
15721671
namespace_index_by_name: dict[str, int] = {}
15731672
namespace_tools_by_name: dict[str, list[FunctionToolParam]] = {}
15741673
namespace_descriptions: dict[str, str] = {}
1674+
use_preview_computer_tool = cls._should_use_preview_computer_tool(
1675+
model=model,
1676+
tool_choice=tool_choice,
1677+
)
15751678
validate_responses_tool_search_configuration(
15761679
tools,
15771680
allow_opaque_search_surface=allow_opaque_tool_search_surface,
@@ -1613,7 +1716,10 @@ def convert_tools(
16131716
includes.append(include)
16141717
continue
16151718

1616-
converted_non_namespace_tool, include = cls._convert_tool(tool)
1719+
converted_non_namespace_tool, include = cls._convert_tool(
1720+
tool,
1721+
use_preview_computer_tool=use_preview_computer_tool,
1722+
)
16171723
converted_tools.append(converted_non_namespace_tool)
16181724
if include:
16191725
includes.append(include)
@@ -1654,7 +1760,30 @@ def _convert_function_tool(
16541760
return function_tool_param, None
16551761

16561762
@classmethod
1657-
def _convert_tool(cls, tool: Tool) -> tuple[ResponsesToolParam, ResponseIncludable | None]:
1763+
def _convert_preview_computer_tool(cls, tool: ComputerTool[Any]) -> ResponsesToolParam:
1764+
computer = tool.computer
1765+
if not isinstance(computer, (Computer, AsyncComputer)):
1766+
raise UserError(
1767+
"Computer tool is not initialized for serialization. Call "
1768+
"resolve_computer({ tool, run_context }) with a run context first "
1769+
"when building payloads manually."
1770+
)
1771+
return _require_responses_tool_param(
1772+
{
1773+
"type": "computer_use_preview",
1774+
"environment": computer.environment,
1775+
"display_width": computer.dimensions[0],
1776+
"display_height": computer.dimensions[1],
1777+
}
1778+
)
1779+
1780+
@classmethod
1781+
def _convert_tool(
1782+
cls,
1783+
tool: Tool,
1784+
*,
1785+
use_preview_computer_tool: bool = False,
1786+
) -> tuple[ResponsesToolParam, ResponseIncludable | None]:
16581787
"""Returns converted tool and includes"""
16591788

16601789
if isinstance(tool, FunctionTool):
@@ -1688,20 +1817,10 @@ def _convert_tool(cls, tool: Tool) -> tuple[ResponsesToolParam, ResponseIncludab
16881817
)
16891818
return file_search_tool_param, include
16901819
elif isinstance(tool, ComputerTool):
1691-
computer = tool.computer
1692-
if not isinstance(computer, (Computer, AsyncComputer)):
1693-
raise UserError(
1694-
"Computer tool is not initialized for serialization. Call "
1695-
"resolve_computer({ tool, run_context }) with a run context first "
1696-
"when building payloads manually."
1697-
)
16981820
return (
1699-
ComputerToolParam(
1700-
type="computer_use_preview",
1701-
environment=computer.environment,
1702-
display_width=computer.dimensions[0],
1703-
display_height=computer.dimensions[1],
1704-
),
1821+
cls._convert_preview_computer_tool(tool)
1822+
if use_preview_computer_tool
1823+
else _require_responses_tool_param({"type": "computer"}),
17051824
None,
17061825
)
17071826
elif isinstance(tool, HostedMCPTool):

0 commit comments

Comments
 (0)