Skip to content

Commit c08398d

Browse files
committed
4.9.0 - MCP/Reasoning
1 parent fce7982 commit c08398d

7 files changed

Lines changed: 266 additions & 156 deletions

File tree

examples/chat_with_mcp.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env python
2+
3+
4+
from shuttleai import ShuttleAI
5+
from shuttleai.schemas.chat.completions import ChatMessage, MCPHeaders, MCPTool # helpers
6+
7+
8+
def main() -> None:
9+
model = "shuttle-3.5"
10+
11+
client = ShuttleAI()
12+
13+
chat_response = client.chat.completions.create(
14+
model=model,
15+
messages=[ChatMessage(role="user", content="do you have access to any mcp tools")],
16+
tools=[
17+
MCPTool(
18+
server_label="ShuttleAI MCP",
19+
server_url="https://mcp.shuttleai.com/mcp", # ?api_key=shuttle-1234" OR headers below
20+
headers=MCPHeaders(authorization="Bearer shuttle-1234")
21+
)
22+
]
23+
)
24+
print(chat_response.choices[0].message.content)
25+
26+
27+
if __name__ == "__main__":
28+
main()

examples/chat_with_reasoning.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env python
2+
3+
4+
from shuttleai import ShuttleAI
5+
from shuttleai.schemas.chat.completions import ChatMessage # Helper for messages
6+
7+
8+
def main() -> None:
9+
model = "claude-3.7-sonnet"
10+
11+
client = ShuttleAI()
12+
13+
chat_response = client.chat.completions.create(
14+
model=model,
15+
messages=[ChatMessage(role="user", content="what is 5 plus 3")],
16+
reasoning_effort="low"
17+
)
18+
print("Thinking:", chat_response.first_choice.message.reasoning_content)
19+
print("\n")
20+
print("Final Response:", chat_response.choices[0].message.content)
21+
# Output:
22+
# Thinking: This is a simple arithmetic problem. I need to compute 5 + 3.
23+
# 5 + 3 = 8
24+
#
25+
# So the answer is 8.
26+
#
27+
# Final Response: 5 plus 3 equals 8.
28+
29+
30+
if __name__ == "__main__":
31+
main()

shuttleai-version.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

shuttleai/_patch.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,12 @@ def _patch_httpx() -> None:
1010
original_encode_json = httpx._content.encode_json
1111

1212
def new_encode_json(json: Any) -> tuple[dict[str, str], ByteStream]:
13-
try:
14-
body = orjson.dumps(json, option=orjson.OPT_NAIVE_UTC)
15-
except orjson.JSONEncodeError:
13+
if not isinstance(json, bytes):
1614
return original_encode_json(json)
17-
1815
return {
1916
"Content-Type": "application/json",
20-
"Content-Length": str(len(body)),
21-
}, ByteStream(body)
17+
"Content-Length": str(len(json)),
18+
}, ByteStream(json)
2219

2320
httpx._content.encode_json = new_encode_json
2421

shuttleai/client/base.py

Lines changed: 73 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import logging
22
import os
33
from abc import ABC
4-
from typing import Any, Dict, List, Optional, Union
4+
from typing import Any, Dict, List, Literal, Optional, Union
55

66
import orjson
77

88
from shuttleai import __version__
99
from shuttleai._types import TimeoutTypes
1010
from 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

1421
class 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

Comments
 (0)