11import logging
22import os
33from abc import ABC
4- from typing import Any , Dict , List , Optional , Union
4+ from typing import Any , Dict , List , Literal , Optional , Union
55
66import orjson
77
88from shuttleai import __version__
99from shuttleai ._types import TimeoutTypes
1010from shuttleai .exceptions import ShuttleAIException
11- from shuttleai .schemas .chat .completions import ChatMessage , Function , ToolChoice
11+ from shuttleai .schemas .chat .completions import (
12+ ChatMessage ,
13+ ChatNamedToolChoice ,
14+ FunctionTool ,
15+ MCPTool ,
16+ ResponseFormat ,
17+ Tool ,
18+ )
1219
1320
1421class ClientBase (ABC ): # noqa: B024
@@ -87,28 +94,53 @@ def _build_sampling_params(
8794 if v is not None
8895 }
8996
90- def _parse_tools (self , tools : List [Dict [str , Any ]]) -> List [Dict [str , Any ]]:
91- return [
92- {
93- "type" : tool ["type" ],
94- "function" : (
95- tool ["function" ].model_dump (exclude_none = True )
96- if isinstance (tool ["function" ], Function )
97- else tool ["function" ]
98- ),
99- }
100- for tool in tools
101- if tool ["type" ] == "function"
102- ]
103-
104- def _parse_tool_choice (self , tool_choice : Union [str , ToolChoice ]) -> str :
105- return tool_choice .value if isinstance (tool_choice , ToolChoice ) else tool_choice
106-
10797 def _parse_messages (self , messages : List [Any ]) -> List [Dict [str , Any ]]:
108- return [
109- (message .model_dump (exclude_none = True ) if isinstance (message , ChatMessage ) else message )
110- for message in messages
111- ]
98+ parsed = []
99+ for message in messages :
100+ if isinstance (message , ChatMessage ):
101+ msg = message .model_dump (mode = "json" , exclude_none = True )
102+ else :
103+ msg = message
104+ if isinstance (msg .get ("content" ), list ):
105+ for part in msg ["content" ]:
106+ if part .get ("type" ) == "image_url" and isinstance (part .get ("image_url" ), str ):
107+ part ["image_url" ] = {"url" : part ["image_url" ], "detail" : "auto" }
108+ parsed .append (msg )
109+ return parsed
110+
111+ def _parse_tools (self , tools : List [Any ]) -> List [Dict [str , Any ]]:
112+ parsed = []
113+ for tool in tools :
114+ if isinstance (tool , (FunctionTool , MCPTool )):
115+ t = tool .model_dump (mode = "json" , exclude_none = True )
116+ else :
117+ t = tool
118+ # For backward compat if flat function
119+ if t .get ("type" ) == "function" and "function" not in t :
120+ func = {
121+ "name" : t ["name" ],
122+ "description" : t .get ("description" ),
123+ "parameters" : t .get ("parameters" ),
124+ }
125+ t = {"type" : "function" , "function" : func }
126+ parsed .append (t )
127+ return parsed
128+
129+ def _parse_tool_choice (self , tool_choice : Any ) -> Any :
130+ if isinstance (tool_choice , ChatNamedToolChoice ):
131+ return tool_choice .model_dump (mode = "json" , exclude_none = True )
132+ elif isinstance (tool_choice , dict ):
133+ return tool_choice
134+ elif isinstance (tool_choice , str ):
135+ return tool_choice
136+ return None
137+
138+ def _parse_response_format (self , response_format : Any ) -> Optional [Dict [str , Any ]]:
139+ if isinstance (response_format , ResponseFormat ):
140+ return response_format .model_dump (mode = "json" , exclude_none = True )
141+ elif isinstance (response_format , dict ):
142+ return response_format
143+ return None
112144
113145 def _make_request (self , endpoint : str , request_data : Dict [str , Any ]) -> Dict [str , Any ]:
114146 if "model" not in request_data :
@@ -120,30 +152,39 @@ def _make_chat_request(
120152 self ,
121153 messages : List [Any ],
122154 model : Optional [str ] = None ,
123- image : Optional [str ] = None ,
124- internet : Optional [str ] = None ,
125- tools : Optional [List [Dict [str , Any ]]] = None ,
155+ tools : Optional [List [Any ]] = None ,
156+ tool_choice : Optional [Any ] = None ,
126157 temperature : Optional [float ] = None ,
127158 max_tokens : Optional [int ] = None ,
128159 top_p : Optional [float ] = None ,
160+ frequency_penalty : Optional [float ] = None ,
161+ presence_penalty : Optional [float ] = None ,
162+ stop : Optional [Union [str , List [str ]]] = None ,
163+ response_format : Optional [Any ] = None ,
164+ reasoning_effort : Optional [Literal ["none" , "minimal" , "low" , "high" ]] = None ,
129165 stream : Optional [bool ] = None ,
130- tool_choice : Optional [Union [str , ToolChoice ]] = None ,
131166 ) -> Dict [str , Any ]:
132167 request_data : Dict [str , Any ] = {
133168 "messages" : self ._parse_messages (messages ),
134169 }
135170 if model :
136171 request_data ["model" ] = model
137- if image :
138- request_data ["image" ] = image
139- if internet :
140- request_data ["internet" ] = internet
141172 if tools :
142173 request_data ["tools" ] = self ._parse_tools (tools )
143174 if tool_choice :
144175 request_data ["tool_choice" ] = self ._parse_tool_choice (tool_choice )
145- if stream :
176+ if response_format :
177+ request_data ["response_format" ] = self ._parse_response_format (response_format )
178+ if reasoning_effort :
179+ request_data ["reasoning_effort" ] = reasoning_effort
180+ if stream is not None :
146181 request_data ["stream" ] = stream
182+ if stop is not None :
183+ request_data ["stop" ] = stop
184+ if frequency_penalty is not None :
185+ request_data ["frequency_penalty" ] = frequency_penalty
186+ if presence_penalty is not None :
187+ request_data ["presence_penalty" ] = presence_penalty
147188 request_data .update (self ._build_sampling_params (max_tokens , temperature , top_p ))
148189 return self ._make_request ("chat" , request_data )
149190
0 commit comments