Loading the notebook…
Loading the notebook…
Notebooks · Provider API features · 64
Anthropic hosts a set of server-side tools that run inside Anthropic's own sandbox: the model invokes them, Anthropic executes them, and the results come back inline in the response. There is no client-side tool loop…
Its imports and arguments are checked against shipit-agent 3.0. The outputs were saved when it was written, for an earlier release.
Server-side tools are an Anthropic API shape. The web_search / code_execution /
computer_use / bash / text_editor helpers emit Anthropic tool declarations
(type: "web_search_20250305", etc.) and the model runs them inside Anthropic's own
sandbox. They work with:
AnthropicChatLLM), andThey do not apply to OpenAI, Gemini, Groq, Ollama, or other non-Anthropic providers — those have their own (different) server-tool conventions. shipit-agent supports all of those providers for ordinary chat/tool-use; this particular feature is Anthropic-only.
from pathlib import Path
import sys
ROOT = (
Path.cwd().resolve().parent
if Path.cwd().name == "notebooks"
else Path.cwd().resolve()
)
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))Each helper returns a plain dict declaration carrying a top-level type — that is
what marks it as a server-side tool (client tools use the {"function": {...}}
shape and never carry a top-level type). Extra keyword args are forwarded
verbatim, so you can pass allowed_domains, display_number, etc.
import json
from shipit_agent.llms import (
web_search,
code_execution,
computer_use,
bash,
text_editor,
)
declarations = {
"web_search": web_search(max_uses=3, allowed_domains=["docs.anthropic.com"]),
"code_execution": code_execution(),
"computer_use": computer_use(display_width_px=1280, display_height_px=800),
"bash": bash(),
"text_editor": text_editor(),
}
print(json.dumps(declarations, indent=2)){
"web_search": {
"type": "web_search_20250305",
"name": "web_search",
"max_uses": 3,
"allowed_domains": [
"docs.anthropic.com"
]
},
"code_execution": {
"type": "code_execution_20250522",
"name": "code_execution"
},
"computer_use": {
"type": "computer_20250124",
"name": "computer",
"display_width_px": 1280,
"display_height_px": 800
},
"bash": {
"type": "bash_20250124",
"name": "bash"
},
"text_editor": {
"type": "text_editor_20250728",
"name": "str_replace_based_edit_tool"
}
}Note the text_editor tool name is str_replace_based_edit_tool (required by the
SDK for the text_editor_20250728 type), and web_search's name is web_search.
code_execution and computer_use require an anthropic-beta header; web_search
is generally available and needs none. required_betas(...) returns the de-duplicated,
order-stable list the adapter attaches automatically.
from shipit_agent.llms.server_tools import required_betas, is_server_tool, BETA_HEADERS
tools = [web_search(), code_execution(), computer_use(display_width_px=1024, display_height_px=768)]
print("is_server_tool(web_search()):", is_server_tool(web_search()))
print("is_server_tool(client tool):", is_server_tool({"function": {"name": "x"}}))
print("\nBETA_HEADERS map:", json.dumps(BETA_HEADERS, indent=2))
print("required betas for [web_search, code_execution, computer_use]:")
print(required_betas(tools))is_server_tool(web_search()): True
is_server_tool(client tool): False
BETA_HEADERS map: {
"code_execution_20250522": "code-execution-2025-05-22",
"computer_20250124": "computer-use-2025-01-24"
}
required betas for [web_search, code_execution, computer_use]:
['code-execution-2025-05-22', 'computer-use-2025-01-24']AnthropicChatLLM._build_request_kwargs(...) returns the exact payload the adapter
would send. Server-tool declarations are forwarded verbatim (not reshaped into
client-tool schemas), and a betas key is added when any tool needs a beta header.
Construction is side-effect free — the anthropic import only happens inside
complete() — so this runs with no API key.
from shipit_agent.llms import AnthropicChatLLM
from shipit_agent.models import Message
llm = AnthropicChatLLM(model="claude-opus-4-1", api_key="offline-demo-key")
req = llm._build_request_kwargs(
messages=[Message(role="user", content="Search the web and run a quick calc.")],
tools=[web_search(max_uses=3), code_execution()],
system_prompt="You are a research assistant.",
)
print("tool declarations forwarded as-is (note top-level type):")
print(json.dumps(req["tools"], indent=2))
print("\nbeta headers attached by the adapter:")
print(req.get("betas"))tool declarations forwarded as-is (note top-level type):
[
{
"type": "web_search_20250305",
"name": "web_search",
"max_uses": 3
},
{
"type": "code_execution_20250522",
"name": "code_execution",
"cache_control": {
"type": "ephemeral"
}
}
]
beta headers attached by the adapter:
['code-execution-2025-05-22']Mixing a client-side tool with server-side tools works too — the client tool is
translated to Anthropic's flat {name, description, input_schema} shape while the
server tool is forwarded verbatim.
A client tool is detected by the absence of a top-level type (it lives under a
function wrapper). Here is a minimal client-tool declaration:
client_tool = {
"function": {
"name": "lookup_order",
"description": "Look up an order by id.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}}},
}
}
req_mixed = llm._build_request_kwargs(
messages=[Message(role="user", content="Find order 42 and search for its tracking page.")],
tools=[client_tool, web_search()],
system_prompt=None,
)
for t in req_mixed["tools"]:
kind = "server-tool (verbatim)" if "type" in t else "client-tool (translated)"
print(f"{t['name']:<28} -> {kind}")
print("\nbetas:", req_mixed.get("betas"), "(web_search is GA, so none)")lookup_order -> client-tool (translated) web_search -> server-tool (verbatim) betas: None (web_search is GA, so none)
LLMResponse.metadata (offline, fake client)When Anthropic runs a server tool, the response contains a server_tool_use block
and a matching result block (e.g. web_search_tool_result). The adapter parses
these into metadata — it deliberately does not add them to tool_calls, since
the client must not try to execute them. There is no standalone extractor (the
parsing is inline in complete()), so to demonstrate it offline we inject a fake
anthropic module whose client returns canned content blocks — the same pattern the
test-suite uses.
import sys, types
from types import SimpleNamespace as NS
# Fake response content: a server_tool_use block, its web_search_tool_result, and text.
fake_content = [
NS(type="server_tool_use", id="srv_1", name="web_search",
input={"query": "anthropic server tools"}),
NS(type="web_search_tool_result", tool_use_id="srv_1",
content=[{"type": "web_search_result", "title": "Docs", "url": "https://docs.anthropic.com"}]),
NS(type="text", text="Based on the search, server tools run in Anthropic's sandbox."),
]
fake_response = NS(
content=fake_content,
usage=NS(input_tokens=120, output_tokens=30,
cache_read_input_tokens=0, cache_creation_input_tokens=0),
)
fake_anthropic = types.ModuleType("anthropic")
class _Messages:
def create(self, **_kwargs):
return fake_response
class _Beta:
def __init__(self):
self.messages = _Messages()
class _Client:
def __init__(self, **_kwargs):
self.messages = _Messages()
self.beta = _Beta()
fake_anthropic.Anthropic = _Client
sys.modules["anthropic"] = fake_anthropic
out = llm.complete(
messages=[Message(role="user", content="What are server tools?")],
tools=[web_search()],
)
print("text:", out.content)
print("client-side tool_calls (should be empty):", out.tool_calls)
print("\nmetadata['server_tool_use']:")
print(json.dumps(out.metadata.get("server_tool_use"), indent=2))
print("\nmetadata['server_tool_results']:")
print(json.dumps(out.metadata.get("server_tool_results"), indent=2))text: Based on the search, server tools run in Anthropic's sandbox.
client-side tool_calls (should be empty): []
metadata['server_tool_use']:
[
{
"type": "server_tool_use",
"id": "srv_1",
"name": "web_search",
"input": {
"query": "anthropic server tools"
}
}
]
metadata['server_tool_results']:
[
{
"type": "web_search_tool_result",
"tool_use_id": "srv_1",
"content": [
{
"type": "web_search_result",
"title": "Docs",
"url": "https://docs.anthropic.com"
}
]
}
]The key takeaway: server tool invocations never enter tool_calls (so your
agent loop won't try to run them locally); they land in
metadata['server_tool_use'] and metadata['server_tool_results'] instead.
This cell makes a live Anthropic call with the hosted web-search tool. It only runs
when ANTHROPIC_API_KEY is set, so the notebook stays runnable offline. (We restore
the real anthropic module first, since the cells above injected a fake one.)
import os
sys.modules.pop("anthropic", None) # drop the fake so a real import is used
if os.getenv("ANTHROPIC_API_KEY"):
live = AnthropicChatLLM(model="claude-opus-4-1")
r = live.complete(
messages=[Message(role="user", content="Search the web: what is the Anthropic Batches API?")],
tools=[web_search(max_uses=2)],
)
print("text:", r.content[:400])
print("server_tool_use:", r.metadata.get("server_tool_use"))
else:
print("Set ANTHROPIC_API_KEY to run the live web-search call. Skipping (offline).")Set ANTHROPIC_API_KEY to run the live web-search call. Skipping (offline).
web_search() / code_execution() / computer_use() / bash() / text_editor()
return Anthropic server-tool declarations (carrying a top-level type).required_betas(...) — inspect both offline with _build_request_kwargs(...).metadata['server_tool_use'] / metadata['server_tool_results'],
never in tool_calls.