Skip to content

Commit 2027f76

Browse files
committed
feat(tool runner): add support for server-side tools
1 parent 536720d commit 2027f76

File tree

8 files changed

+85
-22
lines changed

8 files changed

+85
-22
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
- name: Install uv
2525
uses: astral-sh/setup-uv@v5
2626
with:
27-
version: '0.8.11'
27+
version: '0.9.13'
2828

2929
- name: Install dependencies
3030
run: uv sync --all-extras
@@ -46,7 +46,7 @@ jobs:
4646
- name: Install uv
4747
uses: astral-sh/setup-uv@v5
4848
with:
49-
version: '0.8.11'
49+
version: '0.9.13'
5050

5151
- name: Install dependencies
5252
run: uv sync --all-extras
@@ -80,7 +80,7 @@ jobs:
8080
- name: Install uv
8181
uses: astral-sh/setup-uv@v5
8282
with:
83-
version: '0.8.11'
83+
version: '0.9.13'
8484

8585
- name: Bootstrap
8686
run: ./scripts/bootstrap

.github/workflows/create-releases.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
if: ${{ steps.release.outputs.releases_created }}
2727
uses: astral-sh/setup-uv@v5
2828
with:
29-
version: '0.8.11'
29+
version: '0.9.13'
3030

3131
- name: Publish to PyPI
3232
if: ${{ steps.release.outputs.releases_created }}

.github/workflows/detect-breaking-changes.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: Install uv
2424
uses: astral-sh/setup-uv@v5
2525
with:
26-
version: '0.8.11'
26+
version: '0.9.13'
2727
- name: Install dependencies
2828
run: uv sync --all-extras
2929
- name: Detect removed symbols

.github/workflows/publish-pypi.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- name: Install uv
1717
uses: astral-sh/setup-uv@v5
1818
with:
19-
version: '0.8.11'
19+
version: '0.9.13'
2020

2121
- name: Publish to PyPI
2222
run: |

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Repository = "https://github.com/anthropics/anthropic-sdk-python"
4747

4848
[tool.uv]
4949
managed = true
50-
required-version = ">=0.5.0"
50+
required-version = ">=0.9"
5151
conflicts = [
5252
[
5353
{ group = "pydantic-v1" },

src/anthropic/resources/beta/messages/messages.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,14 @@
4747
from ....lib._parse._response import ResponseFormatT, parse_response
4848
from ....lib._parse._transform import transform_schema
4949
from ....types.beta.beta_message import BetaMessage
50-
from ....lib.tools._beta_functions import BetaRunnableTool, BetaAsyncRunnableTool
50+
from ....lib.tools._beta_functions import (
51+
BetaFunctionTool,
52+
BetaRunnableTool,
53+
BetaAsyncFunctionTool,
54+
BetaAsyncRunnableTool,
55+
BetaBuiltinFunctionTool,
56+
BetaAsyncBuiltinFunctionTool,
57+
)
5158
from ....types.anthropic_beta_param import AnthropicBetaParam
5259
from ....types.beta.beta_message_param import BetaMessageParam
5360
from ....types.beta.beta_metadata_param import BetaMetadataParam
@@ -1174,7 +1181,7 @@ def tool_runner(
11741181
max_tokens: int,
11751182
messages: Iterable[BetaMessageParam],
11761183
model: ModelParam,
1177-
tools: Iterable[BetaRunnableTool],
1184+
tools: Iterable[BetaRunnableTool | BetaToolUnionParam],
11781185
compaction_control: CompactionControl | Omit = omit,
11791186
container: Optional[message_create_params.Container] | Omit = omit,
11801187
context_management: Optional[BetaContextManagementConfigParam] | Omit = omit,
@@ -1208,7 +1215,7 @@ def tool_runner(
12081215
max_tokens: int,
12091216
messages: Iterable[BetaMessageParam],
12101217
model: ModelParam,
1211-
tools: Iterable[BetaRunnableTool],
1218+
tools: Iterable[BetaRunnableTool | BetaToolUnionParam],
12121219
compaction_control: CompactionControl | Omit = omit,
12131220
stream: Literal[True],
12141221
max_iterations: int | Omit = omit,
@@ -1242,7 +1249,7 @@ def tool_runner(
12421249
max_tokens: int,
12431250
messages: Iterable[BetaMessageParam],
12441251
model: ModelParam,
1245-
tools: Iterable[BetaRunnableTool],
1252+
tools: Iterable[BetaRunnableTool | BetaToolUnionParam],
12461253
compaction_control: CompactionControl | Omit = omit,
12471254
stream: bool,
12481255
max_iterations: int | Omit = omit,
@@ -1275,7 +1282,7 @@ def tool_runner(
12751282
max_tokens: int,
12761283
messages: Iterable[BetaMessageParam],
12771284
model: ModelParam,
1278-
tools: Iterable[BetaRunnableTool],
1285+
tools: Iterable[BetaRunnableTool | BetaToolUnionParam],
12791286
compaction_control: CompactionControl | Omit = omit,
12801287
max_iterations: int | Omit = omit,
12811288
container: Optional[message_create_params.Container] | Omit = omit,
@@ -1315,6 +1322,15 @@ def tool_runner(
13151322
**(extra_headers or {}),
13161323
}
13171324

1325+
runnable_tools: list[BetaRunnableTool] = []
1326+
regular_tools: list[BetaToolUnionParam] = []
1327+
1328+
for tool in tools:
1329+
if isinstance(tool, (BetaFunctionTool, BetaBuiltinFunctionTool)):
1330+
runnable_tools.append(tool)
1331+
else:
1332+
regular_tools.append(tool)
1333+
13181334
params = cast(
13191335
message_create_params.ParseMessageCreateParamsBase[ResponseFormatT],
13201336
{
@@ -1333,15 +1349,15 @@ def tool_runner(
13331349
"temperature": temperature,
13341350
"thinking": thinking,
13351351
"tool_choice": tool_choice,
1336-
"tools": [tool.to_dict() for tool in tools],
1352+
"tools": [*[tool.to_dict() for tool in runnable_tools], *regular_tools],
13371353
"top_k": top_k,
13381354
"top_p": top_p,
13391355
},
13401356
)
13411357

13421358
if stream:
13431359
return BetaStreamingToolRunner[ResponseFormatT](
1344-
tools=tools,
1360+
tools=runnable_tools,
13451361
params=params,
13461362
options={
13471363
"extra_headers": extra_headers,
@@ -1354,7 +1370,7 @@ def tool_runner(
13541370
compaction_control=compaction_control if is_given(compaction_control) else None,
13551371
)
13561372
return BetaToolRunner[ResponseFormatT](
1357-
tools=tools,
1373+
tools=runnable_tools,
13581374
params=params,
13591375
options={
13601376
"extra_headers": extra_headers,
@@ -2821,7 +2837,7 @@ def tool_runner(
28212837
max_tokens: int,
28222838
messages: Iterable[BetaMessageParam],
28232839
model: ModelParam,
2824-
tools: Iterable[BetaAsyncRunnableTool],
2840+
tools: Iterable[BetaAsyncRunnableTool | BetaToolUnionParam],
28252841
compaction_control: CompactionControl | Omit = omit,
28262842
max_iterations: int | Omit = omit,
28272843
container: Optional[message_create_params.Container] | Omit = omit,
@@ -2855,7 +2871,7 @@ def tool_runner(
28552871
max_tokens: int,
28562872
messages: Iterable[BetaMessageParam],
28572873
model: ModelParam,
2858-
tools: Iterable[BetaAsyncRunnableTool],
2874+
tools: Iterable[BetaAsyncRunnableTool | BetaToolUnionParam],
28592875
compaction_control: CompactionControl | Omit = omit,
28602876
stream: Literal[True],
28612877
max_iterations: int | Omit = omit,
@@ -2889,7 +2905,7 @@ def tool_runner(
28892905
max_tokens: int,
28902906
messages: Iterable[BetaMessageParam],
28912907
model: ModelParam,
2892-
tools: Iterable[BetaAsyncRunnableTool],
2908+
tools: Iterable[BetaAsyncRunnableTool | BetaToolUnionParam],
28932909
compaction_control: CompactionControl | Omit = omit,
28942910
stream: bool,
28952911
max_iterations: int | Omit = omit,
@@ -2922,7 +2938,7 @@ def tool_runner(
29222938
max_tokens: int,
29232939
messages: Iterable[BetaMessageParam],
29242940
model: ModelParam,
2925-
tools: Iterable[BetaAsyncRunnableTool],
2941+
tools: Iterable[BetaAsyncRunnableTool | BetaToolUnionParam],
29262942
compaction_control: CompactionControl | Omit = omit,
29272943
max_iterations: int | Omit = omit,
29282944
container: Optional[message_create_params.Container] | Omit = omit,
@@ -2962,6 +2978,15 @@ def tool_runner(
29622978
**(extra_headers or {}),
29632979
}
29642980

2981+
runnable_tools: list[BetaAsyncRunnableTool] = []
2982+
regular_tools: list[BetaToolUnionParam] = []
2983+
2984+
for tool in tools:
2985+
if isinstance(tool, (BetaAsyncFunctionTool, BetaAsyncBuiltinFunctionTool)):
2986+
runnable_tools.append(tool)
2987+
else:
2988+
regular_tools.append(tool)
2989+
29652990
params = cast(
29662991
message_create_params.ParseMessageCreateParamsBase[ResponseFormatT],
29672992
{
@@ -2980,15 +3005,15 @@ def tool_runner(
29803005
"temperature": temperature,
29813006
"thinking": thinking,
29823007
"tool_choice": tool_choice,
2983-
"tools": [tool.to_dict() for tool in tools],
3008+
"tools": [*[tool.to_dict() for tool in runnable_tools], *regular_tools],
29843009
"top_k": top_k,
29853010
"top_p": top_p,
29863011
},
29873012
)
29883013

29893014
if stream:
29903015
return BetaAsyncStreamingToolRunner[ResponseFormatT](
2991-
tools=tools,
3016+
tools=runnable_tools,
29923017
params=params,
29933018
options={
29943019
"extra_headers": extra_headers,
@@ -3001,7 +3026,7 @@ def tool_runner(
30013026
compaction_control=compaction_control if is_given(compaction_control) else None,
30023027
)
30033028
return BetaAsyncToolRunner[ResponseFormatT](
3004-
tools=tools,
3029+
tools=runnable_tools,
30053030
params=params,
30063031
options={
30073032
"extra_headers": extra_headers,

tests/lib/tools/__inline_snapshot__/test_runners/TestSyncRunTools.test_server_side_tool/a0a711eb-ee0e-4a42-88d6-5c7f83c0f25a.txt

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

tests/lib/tools/test_runners.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,43 @@ def tool_runner(client: Anthropic) -> BetaToolRunner[None]:
510510
]
511511
)
512512

513+
@pytest.mark.parametrize("client", [False], indirect=True)
514+
@pytest.mark.respx(base_url=base_url)
515+
def test_server_side_tool(
516+
self,
517+
client: Anthropic,
518+
respx_mock: MockRouter,
519+
) -> None:
520+
def tool_runner(client: Anthropic) -> BetaToolRunner[None]:
521+
runner = client.beta.messages.tool_runner(
522+
model="claude-haiku-4-5",
523+
messages=[{"role": "user", "content": "What is the weather in SF?"}],
524+
tools=[
525+
{
526+
"type": "web_search_20250305",
527+
"name": "web_search",
528+
}
529+
],
530+
max_tokens=1024,
531+
)
532+
533+
message = next(runner)
534+
535+
content_types = [content.type for content in message.content]
536+
537+
assert "server_tool_use" in content_types
538+
assert "web_search_tool_result" in content_types
539+
540+
return runner
541+
542+
make_snapshot_request(
543+
tool_runner,
544+
content_snapshot=external("uuid:a0a711eb-ee0e-4a42-88d6-5c7f83c0f25a.txt"),
545+
path="/v1/messages",
546+
mock_client=client,
547+
respx_mock=respx_mock,
548+
)
549+
513550

514551
@pytest.mark.skipif(PYDANTIC_V1, reason="tool runner not supported with pydantic v1")
515552
@pytest.mark.respx(base_url=base_url)

0 commit comments

Comments
 (0)