@@ -983,6 +983,11 @@ class OpenAI(Llm, OpenAIMixin):
983983 An LLM implementation using the OpenAI cloud.
984984 """
985985
986+ api = param .Selector (default = "chat_completions" , objects = ["chat_completions" , "responses" ], doc = """
987+ OpenAI API primitive to use.
988+ - ``chat_completions``: Uses ``/v1/chat/completions`` (default)
989+ - ``responses``: Uses ``/v1/responses``""" )
990+
986991 display_name = param .String (default = "OpenAI" , constant = True )
987992
988993 mode = param .Selector (default = Mode .TOOLS )
@@ -1005,6 +1010,49 @@ class OpenAI(Llm, OpenAIMixin):
10051010
10061011 _supports_logfire = True
10071012
1013+ @classmethod
1014+ def _resolve_openai_mode (cls , mode : Mode ) -> Mode :
1015+ if mode in (Mode .RESPONSES_TOOLS , Mode .RESPONSES_TOOLS_WITH_INBUILT_TOOLS ):
1016+ return mode
1017+ return Mode .RESPONSES_TOOLS
1018+
1019+ @classmethod
1020+ def _transform_responses_tools (cls , tools : list [dict [str , Any ]] | None ) -> list [dict [str , Any ]] | None :
1021+ if not tools :
1022+ return tools
1023+ transformed : list [dict [str , Any ]] = []
1024+ for tool in tools :
1025+ if (
1026+ isinstance (tool , dict )
1027+ and tool .get ("type" ) == "function"
1028+ and isinstance (tool .get ("function" ), dict )
1029+ ):
1030+ # Chat Completions format -> Responses format
1031+ function = tool ["function" ]
1032+ transformed .append ({
1033+ "type" : "function" ,
1034+ "name" : function .get ("name" ),
1035+ "description" : function .get ("description" , "" ),
1036+ "parameters" : function .get ("parameters" , {}),
1037+ })
1038+ else :
1039+ transformed .append (tool )
1040+ return transformed
1041+
1042+ @classmethod
1043+ def _tool_messages_to_response_inputs (cls , tool_messages : list [Message ]) -> list [dict [str , Any ]]:
1044+ inputs : list [dict [str , Any ]] = []
1045+ for message in tool_messages :
1046+ call_id = message .get ("tool_call_id" )
1047+ if not call_id :
1048+ continue
1049+ inputs .append ({
1050+ "type" : "function_call_output" ,
1051+ "call_id" : call_id ,
1052+ "output" : message ["content" ],
1053+ })
1054+ return inputs
1055+
10081056 def models (self ) -> set [str ]:
10091057 """Return the set of available model identifiers from OpenAI."""
10101058 client = OpenAIClient (api_key = self .api_key , timeout = 5 )
@@ -1025,24 +1073,222 @@ def _create_base_client(self, **kwargs) -> Any:
10251073 self ._logfire .instrument_openai (client )
10261074 return client
10271075
1076+ def _get_completion_method (self ) -> Callable :
1077+ if self .api == "responses" :
1078+ return self ._base_client .responses .create
1079+ return super ()._get_completion_method ()
1080+
10281081 def _create_instructor_client (self , base_client : Any , mode : Mode ) -> Any :
1082+ if self .api == "responses" :
1083+ mode = self ._resolve_openai_mode (mode )
10291084 if self .interceptor :
10301085 self .interceptor .patch_client (base_client , mode = "store_inputs" )
10311086 wrapped = instructor .from_openai (base_client , mode = mode )
10321087 if self .interceptor :
10331088 self .interceptor .patch_client_response (wrapped )
10341089 return wrapped
10351090
1036- async def get_client (self , model_spec : str | dict , response_model : BaseModel | None = None , ** kwargs ):
1091+ @classmethod
1092+ def _get_content (cls , response ) -> str | BaseModel :
1093+ if hasattr (response , "output_text" ):
1094+ return response .output_text or ""
1095+ return super ()._get_content (response )
1096+
1097+ @classmethod
1098+ def _get_delta (cls , chunk ) -> str :
1099+ event_type = getattr (chunk , "type" , None )
1100+ if event_type == "response.output_text.delta" :
1101+ return getattr (chunk , "delta" , "" ) or ""
1102+ if isinstance (chunk , dict ):
1103+ if chunk .get ("type" ) == "response.output_text.delta" :
1104+ return chunk .get ("delta" ) or ""
1105+ return super ()._get_delta (chunk )
1106+
1107+ @classmethod
1108+ def _extract_tool_calls (cls , response : Any ) -> list [Any ]:
1109+ output_items = getattr (response , "output" , None )
1110+ if output_items is None and isinstance (response , dict ):
1111+ output_items = response .get ("output" )
1112+ if output_items :
1113+ tool_calls : list [dict [str , Any ]] = []
1114+ for item in output_items :
1115+ item_type = item .get ("type" ) if isinstance (item , dict ) else getattr (item , "type" , None )
1116+ if item_type != "function_call" :
1117+ continue
1118+ call_id = item .get ("call_id" ) if isinstance (item , dict ) else getattr (item , "call_id" , None )
1119+ name = item .get ("name" ) if isinstance (item , dict ) else getattr (item , "name" , None )
1120+ arguments = item .get ("arguments" ) if isinstance (item , dict ) else getattr (item , "arguments" , "" )
1121+ tool_calls .append ({
1122+ "id" : call_id ,
1123+ "type" : "function" ,
1124+ "function" : {"name" : name , "arguments" : arguments or "" },
1125+ })
1126+ if tool_calls :
1127+ return tool_calls
1128+ return super ()._extract_tool_calls (response )
1129+
1130+ @classmethod
1131+ def _extract_stream_tool_calls (cls , chunk : Any ) -> list [dict [str , Any ]]:
1132+ event_type = getattr (chunk , "type" , None )
1133+ if event_type is None and isinstance (chunk , dict ):
1134+ event_type = chunk .get ("type" )
1135+
1136+ if event_type == "response.output_item.added" :
1137+ item = getattr (chunk , "item" , None )
1138+ output_index = getattr (chunk , "output_index" , 0 )
1139+ if item is not None and getattr (item , "type" , None ) == "function_call" :
1140+ return [{
1141+ "index" : output_index ,
1142+ "id" : getattr (item , "call_id" , None ),
1143+ "type" : "function" ,
1144+ "function" : {"name" : getattr (item , "name" , None ), "arguments" : "" },
1145+ }]
1146+ elif event_type == "response.function_call_arguments.delta" :
1147+ return [{
1148+ "index" : getattr (chunk , "output_index" , 0 ),
1149+ "type" : "function" ,
1150+ "function" : {"name" : None , "arguments" : getattr (chunk , "delta" , "" ) or "" },
1151+ }]
1152+ elif event_type == "response.function_call_arguments.done" :
1153+ return [{
1154+ "index" : getattr (chunk , "output_index" , 0 ),
1155+ "id" : getattr (chunk , "item_id" , None ),
1156+ "type" : "function" ,
1157+ "function" : {
1158+ "name" : getattr (chunk , "name" , None ),
1159+ "arguments" : getattr (chunk , "arguments" , "" ) or "" ,
1160+ },
1161+ }]
1162+
1163+ if isinstance (chunk , dict ):
1164+ if chunk .get ("type" ) == "response.output_item.added" :
1165+ item = chunk .get ("item" ) or {}
1166+ if item .get ("type" ) == "function_call" :
1167+ return [{
1168+ "index" : chunk .get ("output_index" , 0 ),
1169+ "id" : item .get ("call_id" ),
1170+ "type" : "function" ,
1171+ "function" : {"name" : item .get ("name" ), "arguments" : "" },
1172+ }]
1173+ elif chunk .get ("type" ) == "response.function_call_arguments.delta" :
1174+ return [{
1175+ "index" : chunk .get ("output_index" , 0 ),
1176+ "type" : "function" ,
1177+ "function" : {"name" : None , "arguments" : chunk .get ("delta" ) or "" },
1178+ }]
1179+ elif chunk .get ("type" ) == "response.function_call_arguments.done" :
1180+ return [{
1181+ "index" : chunk .get ("output_index" , 0 ),
1182+ "id" : chunk .get ("item_id" ),
1183+ "type" : "function" ,
1184+ "function" : {
1185+ "name" : chunk .get ("name" ),
1186+ "arguments" : chunk .get ("arguments" ) or "" ,
1187+ },
1188+ }]
1189+
1190+ return super ()._extract_stream_tool_calls (chunk )
1191+
1192+ async def _run_tool_loop (
1193+ self ,
1194+ messages : list [Message ],
1195+ structured_model : type [BaseModel ] | None ,
1196+ tool_instances : dict ,
1197+ tool_contexts : dict ,
1198+ model_spec : str | dict = "default" ,
1199+ max_tool_rounds : int = 16 ,
1200+ ** kwargs
1201+ ) -> BaseModel | str :
1202+ if self .api != "responses" :
1203+ return await super ()._run_tool_loop (
1204+ messages , structured_model , tool_instances , tool_contexts , model_spec , max_tool_rounds , ** kwargs
1205+ )
1206+
1207+ if structured_model is not None and not tool_instances :
1208+ kwargs ["response_model" ] = structured_model
1209+ else :
1210+ kwargs .pop ("response_model" , None )
1211+
1212+ output = await self .run_client (model_spec , messages , ** kwargs )
1213+ if not tool_instances :
1214+ return output
1215+
1216+ for _ in range (max_tool_rounds ):
1217+ tool_calls = self ._extract_tool_calls (output )
1218+ if not tool_calls :
1219+ break
1220+ tool_messages = await self ._run_tool_calls (
1221+ tool_instances , tool_calls , tool_contexts , messages
1222+ )
1223+ if not tool_messages :
1224+ break
1225+ tool_outputs = self ._tool_messages_to_response_inputs (tool_messages )
1226+ if not tool_outputs :
1227+ break
1228+ next_kwargs = dict (kwargs )
1229+ response_id = getattr (output , "id" , None )
1230+ if response_id :
1231+ next_kwargs ["previous_response_id" ] = response_id
1232+ output = await self .run_client (model_spec , tool_outputs , ** next_kwargs )
1233+
1234+ if structured_model :
1235+ final_kwargs = dict (kwargs )
1236+ final_kwargs ["response_model" ] = structured_model
1237+ response_id = getattr (output , "id" , None )
1238+ if response_id :
1239+ final_kwargs ["previous_response_id" ] = response_id
1240+ output = await self .run_client (model_spec , [], ** final_kwargs )
1241+ return output
1242+
1243+ async def get_client (self , model_spec : str | dict , response_model : type [BaseModel ] | None = None , ** kwargs ):
10371244 model_kwargs = self ._get_model_kwargs (model_spec )
10381245 model = model_kwargs .pop ("model" )
10391246 log_debug (f"LLM Model: \033 [96m{ model !r} \033 [0m" )
1040- model_kwargs ["mode" ] = model_kwargs .pop ("mode" , self .mode )
1247+ mode = model_kwargs .pop ("mode" , self .mode )
1248+
1249+ if self .api == "responses" :
1250+ if self ._base_client is None :
1251+ self ._base_client = self ._create_base_client (** model_kwargs )
1252+ if response_model :
1253+ mode = self ._resolve_openai_mode (mode )
1254+ if mode not in self ._instructor_clients :
1255+ self ._instructor_clients [mode ] = self ._create_instructor_client (self ._base_client , mode )
1256+ client = self ._instructor_clients [mode ]
1257+ client_callable = partial (client .responses .create , model = model , ** self ._get_create_kwargs (response_model ))
1258+ else :
1259+ client_callable = partial (self ._base_client .responses .create , model = model , ** self ._get_create_kwargs (response_model ))
1260+ else :
1261+ model_kwargs ["mode" ] = mode
1262+ client_callable = self ._get_cached_client (response_model , model = model , ** model_kwargs )
10411263
1042- client_callable = self ._get_cached_client (response_model , model = model , ** model_kwargs )
10431264 # Add timeout to the partial
10441265 return partial (client_callable .func , * client_callable .args , timeout = self .timeout , ** client_callable .keywords )
10451266
1267+ async def run_client (self , model_spec : str | dict , messages : list [Message ] | list [dict [str , Any ]], ** kwargs ):
1268+ if self .api == "responses" :
1269+ log_debug (f"Input messages: \033 [95m{ len (messages )} messages\033 [0m including system" )
1270+ for i , message in enumerate (messages ):
1271+ role = message .get ("role" ) if isinstance (message , dict ) else None
1272+ content = message .get ("content" ) if isinstance (message , dict ) else None
1273+ if role == "system" :
1274+ continue
1275+ if role in ("user" , "assistant" , "tool" ):
1276+ role_char = "u" if role == "user" else "a"
1277+ log_debug (f"Message \033 [95m{ i } ({ role_char } )\033 [0m: { format_msg_content (content )} " )
1278+ else :
1279+ item_type = message .get ("type" ) if isinstance (message , dict ) else type (message ).__name__
1280+ log_debug (f"Message \033 [95m{ i } \033 [0m: [{ item_type } ] { truncate_string (str (message ), max_length = 2000 )} " )
1281+
1282+ if kwargs .get ("tools" ):
1283+ kwargs = dict (kwargs )
1284+ kwargs ["tools" ] = self ._transform_responses_tools (kwargs .get ("tools" ))
1285+ client = await self .get_client (model_spec , ** kwargs )
1286+ result = await client (input = messages , ** kwargs )
1287+ log_debug (f"LLM Response: \033 [95m{ truncate_string (str (result ), max_length = 1000 )} \033 [0m\n ---" )
1288+ return result
1289+
1290+ return await super ().run_client (model_spec , messages , ** kwargs )
1291+
10461292
10471293class AzureOpenAI (Llm , AzureOpenAIMixin ):
10481294 """
0 commit comments