1616from openai .types import ChatModel
1717from 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