ensure we support tool search

This commit is contained in:
lorenzejay
2026-03-08 14:15:30 -07:00
parent bc45a7fbe3
commit 97df7700aa
4 changed files with 680 additions and 6 deletions

View File

@@ -22,7 +22,12 @@ if TYPE_CHECKING:
try:
from anthropic import Anthropic, AsyncAnthropic, transform_schema
from anthropic.types import Message, TextBlock, ThinkingBlock, ToolUseBlock
from anthropic.types import (
Message,
TextBlock,
ThinkingBlock,
ToolUseBlock,
)
from anthropic.types.beta import BetaMessage, BetaTextBlock, BetaToolUseBlock
import httpx
except ImportError:
@@ -31,6 +36,11 @@ except ImportError:
) from None
TOOL_SEARCH_TOOL_TYPES: Final[tuple[str, ...]] = (
"tool_search_tool_regex_20251119",
"tool_search_tool_bm25_20251119",
)
ANTHROPIC_FILES_API_BETA: Final = "files-api-2025-04-14"
ANTHROPIC_STRUCTURED_OUTPUTS_BETA: Final = "structured-outputs-2025-11-13"
@@ -117,6 +127,22 @@ class AnthropicThinkingConfig(BaseModel):
budget_tokens: int | None = None
class AnthropicToolSearchConfig(BaseModel):
"""Configuration for Anthropic's server-side tool search.
When enabled, tools marked with defer_loading=True are not loaded into
context immediately. Instead, Claude uses the tool search tool to
dynamically discover and load relevant tools on-demand.
Attributes:
type: The tool search variant to use.
- "regex": Claude constructs regex patterns to search tool names/descriptions.
- "bm25": Claude uses natural language queries to search tools.
"""
type: Literal["regex", "bm25"] = "bm25"
class AnthropicCompletion(BaseLLM):
"""Anthropic native completion implementation.
@@ -140,6 +166,7 @@ class AnthropicCompletion(BaseLLM):
interceptor: BaseInterceptor[httpx.Request, httpx.Response] | None = None,
thinking: AnthropicThinkingConfig | None = None,
response_format: type[BaseModel] | None = None,
tool_search: AnthropicToolSearchConfig | bool | None = None,
**kwargs: Any,
):
"""Initialize Anthropic chat completion client.
@@ -159,6 +186,10 @@ class AnthropicCompletion(BaseLLM):
interceptor: HTTP interceptor for modifying requests/responses at transport level.
response_format: Pydantic model for structured output. When provided, responses
will be validated against this model schema.
tool_search: Enable Anthropic's server-side tool search. When True, uses "bm25"
variant by default. Pass an AnthropicToolSearchConfig to choose "regex" or
"bm25". When enabled, tools are automatically marked with defer_loading=True
and a tool search tool is injected into the tools list.
**kwargs: Additional parameters
"""
super().__init__(
@@ -190,6 +221,13 @@ class AnthropicCompletion(BaseLLM):
self.thinking = thinking
self.previous_thinking_blocks: list[ThinkingBlock] = []
self.response_format = response_format
# Tool search config
if tool_search is True:
self.tool_search = AnthropicToolSearchConfig()
elif isinstance(tool_search, AnthropicToolSearchConfig):
self.tool_search = tool_search
else:
self.tool_search = None
# Model-specific settings
self.is_claude_3 = "claude-3" in model.lower()
self.supports_tools = True
@@ -432,10 +470,22 @@ class AnthropicCompletion(BaseLLM):
# Handle tools for Claude 3+
if tools and self.supports_tools:
converted_tools = self._convert_tools_for_interference(tools)
# When tool_search is enabled, inject the tool search tool and
# mark all regular tools with defer_loading=True
if self.tool_search is not None:
converted_tools = self._apply_tool_search(converted_tools)
params["tools"] = converted_tools
if available_functions and len(converted_tools) == 1:
tool_name = converted_tools[0].get("name")
# Count only regular tools (not tool search tools) for tool_choice
regular_tools = [
t
for t in converted_tools
if t.get("type", "") not in TOOL_SEARCH_TOOL_TYPES
]
if available_functions and len(regular_tools) == 1:
tool_name = regular_tools[0].get("name")
if tool_name and tool_name in available_functions:
params["tool_choice"] = {"type": "tool", "name": tool_name}
@@ -454,6 +504,12 @@ class AnthropicCompletion(BaseLLM):
anthropic_tools = []
for tool in tools:
# Pass through tool search tool definitions unchanged
tool_type = tool.get("type", "")
if tool_type in TOOL_SEARCH_TOOL_TYPES:
anthropic_tools.append(tool)
continue
if "input_schema" in tool and "name" in tool and "description" in tool:
anthropic_tools.append(tool)
continue
@@ -466,15 +522,15 @@ class AnthropicCompletion(BaseLLM):
logging.error(f"Error converting tool to Anthropic format: {e}")
raise e
anthropic_tool = {
anthropic_tool: dict[str, Any] = {
"name": name,
"description": description,
}
if parameters and isinstance(parameters, dict):
anthropic_tool["input_schema"] = parameters # type: ignore[assignment]
anthropic_tool["input_schema"] = parameters
else:
anthropic_tool["input_schema"] = { # type: ignore[assignment]
anthropic_tool["input_schema"] = {
"type": "object",
"properties": {},
"required": [],
@@ -484,6 +540,54 @@ class AnthropicCompletion(BaseLLM):
return anthropic_tools
def _apply_tool_search(self, tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Inject tool search tool and mark regular tools with defer_loading.
When tool_search is enabled, this method:
1. Adds the appropriate tool search tool definition (regex or bm25)
2. Marks all regular tools with defer_loading=True so they are only
loaded when Claude discovers them via search
Args:
tools: Converted tool definitions in Anthropic format.
Returns:
Updated tools list with tool search tool prepended and
regular tools marked as deferred.
"""
assert self.tool_search is not None # type
# Check if a tool search tool is already present (user passed one manually)
has_search_tool = any(
t.get("type", "") in TOOL_SEARCH_TOOL_TYPES for t in tools
)
result: list[dict[str, Any]] = []
if not has_search_tool:
# Map config type to API type identifier
type_map = {
"regex": "tool_search_tool_regex_20251119",
"bm25": "tool_search_tool_bm25_20251119",
}
tool_type = type_map[self.tool_search.type]
# Tool search tool names follow the convention: tool_search_tool_{variant}
tool_name = f"tool_search_tool_{self.tool_search.type}"
result.append({"type": tool_type, "name": tool_name})
for tool in tools:
# Don't modify tool search tools
if tool.get("type", "") in TOOL_SEARCH_TOOL_TYPES:
result.append(tool)
continue
# Mark regular tools as deferred if not already set
if "defer_loading" not in tool:
tool = {**tool, "defer_loading": True}
result.append(tool)
return result
def _extract_thinking_block(
self, content_block: Any
) -> ThinkingBlock | dict[str, Any] | None:

View File

@@ -0,0 +1,137 @@
interactions:
- request:
body: '{"max_tokens":4096,"messages":[{"role":"user","content":"What is the weather
in Tokyo?"}],"model":"claude-sonnet-4-5","stream":false,"tools":[{"type":"tool_search_tool_bm25_20251119","name":"tool_search_tool_bm25"},{"name":"get_weather","description":"Get
current weather conditions for a specified location","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for get_weather"}},"required":["input"]},"defer_loading":true},{"name":"search_files","description":"Search
through files in the workspace by name or content","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for search_files"}},"required":["input"]},"defer_loading":true},{"name":"read_database","description":"Read
records from a database table with optional filtering","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for read_database"}},"required":["input"]},"defer_loading":true},{"name":"write_database","description":"Write
or update records in a database table","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for write_database"}},"required":["input"]},"defer_loading":true},{"name":"send_email","description":"Send
an email message to one or more recipients","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for send_email"}},"required":["input"]},"defer_loading":true},{"name":"read_email","description":"Read
emails from inbox with filtering options","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for read_email"}},"required":["input"]},"defer_loading":true},{"name":"create_ticket","description":"Create
a new support ticket in the ticketing system","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for create_ticket"}},"required":["input"]},"defer_loading":true},{"name":"update_ticket","description":"Update
an existing support ticket status or description","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for update_ticket"}},"required":["input"]},"defer_loading":true},{"name":"list_users","description":"List
all users in the system with optional filters","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for list_users"}},"required":["input"]},"defer_loading":true},{"name":"get_user_profile","description":"Get
detailed profile information for a specific user","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for get_user_profile"}},"required":["input"]},"defer_loading":true},{"name":"deploy_service","description":"Deploy
a service to the specified environment","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for deploy_service"}},"required":["input"]},"defer_loading":true},{"name":"rollback_service","description":"Rollback
a service deployment to a previous version","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for rollback_service"}},"required":["input"]},"defer_loading":true},{"name":"get_service_logs","description":"Get
service logs filtered by time range and severity","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for get_service_logs"}},"required":["input"]},"defer_loading":true},{"name":"run_sql_query","description":"Run
a read-only SQL query against the analytics database","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for run_sql_query"}},"required":["input"]},"defer_loading":true},{"name":"create_dashboard","description":"Create
a new monitoring dashboard with widgets","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for create_dashboard"}},"required":["input"]},"defer_loading":true}]}'
headers:
User-Agent:
- X-USER-AGENT-XXX
accept:
- application/json
accept-encoding:
- ACCEPT-ENCODING-XXX
anthropic-version:
- '2023-06-01'
connection:
- keep-alive
content-length:
- '3952'
content-type:
- application/json
host:
- api.anthropic.com
x-api-key:
- X-API-KEY-XXX
x-stainless-arch:
- X-STAINLESS-ARCH-XXX
x-stainless-async:
- 'false'
x-stainless-lang:
- python
x-stainless-os:
- X-STAINLESS-OS-XXX
x-stainless-package-version:
- 0.73.0
x-stainless-retry-count:
- '0'
x-stainless-runtime:
- CPython
x-stainless-runtime-version:
- 3.13.3
x-stainless-timeout:
- NOT_GIVEN
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: '{"model":"claude-sonnet-4-5-20250929","id":"msg_01DAGCoL6C12u6yAgR1UqNAs","type":"message","role":"assistant","content":[{"type":"text","text":"I''ll
search for a weather-related tool to help you get the weather information
for Tokyo."},{"type":"server_tool_use","id":"srvtoolu_0176qgHeeBpSygYAnUzKHCfh","name":"tool_search_tool_bm25","input":{"query":"weather
Tokyo current conditions forecast"},"caller":{"type":"direct"}},{"type":"tool_search_tool_result","tool_use_id":"srvtoolu_0176qgHeeBpSygYAnUzKHCfh","content":{"type":"tool_search_tool_search_result","tool_references":[{"type":"tool_reference","tool_name":"get_weather"}]}},{"type":"text","text":"Great!
I found a weather tool. Let me get the current weather conditions for Tokyo."},{"type":"tool_use","id":"toolu_01R3FavQLuTrwNvEk9gMaViK","name":"get_weather","input":{"input":"Tokyo"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1566,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":155,"service_tier":"standard","inference_geo":"not_available","server_tool_use":{"web_search_requests":0,"web_fetch_requests":0}}}'
headers:
CF-RAY:
- CF-RAY-XXX
Connection:
- keep-alive
Content-Security-Policy:
- CSP-FILTERED
Content-Type:
- application/json
Date:
- Sun, 08 Mar 2026 21:04:12 GMT
Server:
- cloudflare
Transfer-Encoding:
- chunked
X-Robots-Tag:
- none
anthropic-organization-id:
- ANTHROPIC-ORGANIZATION-ID-XXX
anthropic-ratelimit-input-tokens-limit:
- ANTHROPIC-RATELIMIT-INPUT-TOKENS-LIMIT-XXX
anthropic-ratelimit-input-tokens-remaining:
- ANTHROPIC-RATELIMIT-INPUT-TOKENS-REMAINING-XXX
anthropic-ratelimit-input-tokens-reset:
- ANTHROPIC-RATELIMIT-INPUT-TOKENS-RESET-XXX
anthropic-ratelimit-output-tokens-limit:
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-LIMIT-XXX
anthropic-ratelimit-output-tokens-remaining:
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-REMAINING-XXX
anthropic-ratelimit-output-tokens-reset:
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-RESET-XXX
anthropic-ratelimit-requests-limit:
- '20000'
anthropic-ratelimit-requests-remaining:
- '19999'
anthropic-ratelimit-requests-reset:
- '2026-03-08T21:04:07Z'
anthropic-ratelimit-tokens-limit:
- ANTHROPIC-RATELIMIT-TOKENS-LIMIT-XXX
anthropic-ratelimit-tokens-remaining:
- ANTHROPIC-RATELIMIT-TOKENS-REMAINING-XXX
anthropic-ratelimit-tokens-reset:
- ANTHROPIC-RATELIMIT-TOKENS-RESET-XXX
cf-cache-status:
- DYNAMIC
request-id:
- REQUEST-ID-XXX
strict-transport-security:
- STS-XXX
vary:
- Accept-Encoding
x-envoy-upstream-service-time:
- '4330'
status:
code: 200
message: OK
version: 1

View File

@@ -0,0 +1,112 @@
interactions:
- request:
body: '{"max_tokens":4096,"messages":[{"role":"user","content":"What is the weather
in Tokyo?"}],"model":"claude-sonnet-4-5","stream":false,"tools":[{"name":"get_weather","description":"Get
current weather conditions for a specified location","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for get_weather"}},"required":["input"]}},{"name":"search_files","description":"Search
through files in the workspace by name or content","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for search_files"}},"required":["input"]}},{"name":"read_database","description":"Read
records from a database table with optional filtering","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for read_database"}},"required":["input"]}},{"name":"write_database","description":"Write
or update records in a database table","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for write_database"}},"required":["input"]}},{"name":"send_email","description":"Send
an email message to one or more recipients","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for send_email"}},"required":["input"]}},{"name":"read_email","description":"Read
emails from inbox with filtering options","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for read_email"}},"required":["input"]}},{"name":"create_ticket","description":"Create
a new support ticket in the ticketing system","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for create_ticket"}},"required":["input"]}},{"name":"update_ticket","description":"Update
an existing support ticket status or description","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for update_ticket"}},"required":["input"]}},{"name":"list_users","description":"List
all users in the system with optional filters","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for list_users"}},"required":["input"]}},{"name":"get_user_profile","description":"Get
detailed profile information for a specific user","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for get_user_profile"}},"required":["input"]}},{"name":"deploy_service","description":"Deploy
a service to the specified environment","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for deploy_service"}},"required":["input"]}},{"name":"rollback_service","description":"Rollback
a service deployment to a previous version","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for rollback_service"}},"required":["input"]}},{"name":"get_service_logs","description":"Get
service logs filtered by time range and severity","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for get_service_logs"}},"required":["input"]}},{"name":"run_sql_query","description":"Run
a read-only SQL query against the analytics database","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for run_sql_query"}},"required":["input"]}},{"name":"create_dashboard","description":"Create
a new monitoring dashboard with widgets","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for create_dashboard"}},"required":["input"]}}]}'
headers:
accept:
- application/json
anthropic-version:
- '2023-06-01'
connection:
- keep-alive
content-type:
- application/json
host:
- api.anthropic.com
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: '{"model":"claude-sonnet-4-5-20250929","id":"msg_01NoSearch001","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01NoSearch001","name":"get_weather","input":{"input":"Tokyo"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1943,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":54,"service_tier":"standard"}}'
headers:
Content-Type:
- application/json
status:
code: 200
message: OK
- request:
body: '{"max_tokens":4096,"messages":[{"role":"user","content":"What is the weather
in Tokyo?"}],"model":"claude-sonnet-4-5","stream":false,"tools":[{"type":"tool_search_tool_bm25_20251119","name":"tool_search_tool_bm25"},{"name":"get_weather","description":"Get
current weather conditions for a specified location","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for get_weather"}},"required":["input"]},"defer_loading":true},{"name":"search_files","description":"Search
through files in the workspace by name or content","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for search_files"}},"required":["input"]},"defer_loading":true},{"name":"read_database","description":"Read
records from a database table with optional filtering","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for read_database"}},"required":["input"]},"defer_loading":true},{"name":"write_database","description":"Write
or update records in a database table","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for write_database"}},"required":["input"]},"defer_loading":true},{"name":"send_email","description":"Send
an email message to one or more recipients","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for send_email"}},"required":["input"]},"defer_loading":true},{"name":"read_email","description":"Read
emails from inbox with filtering options","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for read_email"}},"required":["input"]},"defer_loading":true},{"name":"create_ticket","description":"Create
a new support ticket in the ticketing system","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for create_ticket"}},"required":["input"]},"defer_loading":true},{"name":"update_ticket","description":"Update
an existing support ticket status or description","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for update_ticket"}},"required":["input"]},"defer_loading":true},{"name":"list_users","description":"List
all users in the system with optional filters","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for list_users"}},"required":["input"]},"defer_loading":true},{"name":"get_user_profile","description":"Get
detailed profile information for a specific user","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for get_user_profile"}},"required":["input"]},"defer_loading":true},{"name":"deploy_service","description":"Deploy
a service to the specified environment","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for deploy_service"}},"required":["input"]},"defer_loading":true},{"name":"rollback_service","description":"Rollback
a service deployment to a previous version","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for rollback_service"}},"required":["input"]},"defer_loading":true},{"name":"get_service_logs","description":"Get
service logs filtered by time range and severity","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for get_service_logs"}},"required":["input"]},"defer_loading":true},{"name":"run_sql_query","description":"Run
a read-only SQL query against the analytics database","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for run_sql_query"}},"required":["input"]},"defer_loading":true},{"name":"create_dashboard","description":"Create
a new monitoring dashboard with widgets","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
for create_dashboard"}},"required":["input"]},"defer_loading":true}]}'
headers:
accept:
- application/json
anthropic-version:
- '2023-06-01'
connection:
- keep-alive
content-type:
- application/json
host:
- api.anthropic.com
method: POST
uri: https://api.anthropic.com/v1/messages
response:
body:
string: '{"model":"claude-sonnet-4-5-20250929","id":"msg_01WithSearch001","type":"message","role":"assistant","content":[{"type":"text","text":"I''ll search for a weather tool."},{"type":"server_tool_use","id":"srvtoolu_01Search001","name":"tool_search_tool_bm25","input":{"query":"weather conditions"},"caller":{"type":"direct"}},{"type":"tool_search_tool_result","tool_use_id":"srvtoolu_01Search001","content":{"type":"tool_search_tool_search_result","tool_references":[{"type":"tool_reference","tool_name":"get_weather"}]}},{"type":"text","text":"Found it. Let me get the weather for Tokyo."},{"type":"tool_use","id":"toolu_01WithSearch001","name":"get_weather","input":{"input":"Tokyo"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1566,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":155,"service_tier":"standard"}}'
headers:
Content-Type:
- application/json
status:
code: 200
message: OK
version: 1

View File

@@ -1121,3 +1121,324 @@ def test_anthropic_cached_prompt_tokens_with_tools():
assert usage.successful_requests == 2
# The second call should have cached prompt tokens
assert usage.cached_prompt_tokens > 0
# ---- Tool Search Tool Tests ----
def test_tool_search_true_injects_bm25_and_defer_loading():
"""tool_search=True should inject bm25 tool search and defer all tools."""
llm = LLM(model="anthropic/claude-sonnet-4-5", tool_search=True)
crewai_tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get weather for a location",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
},
},
},
{
"type": "function",
"function": {
"name": "calculator",
"description": "Perform math calculations",
"parameters": {
"type": "object",
"properties": {"expression": {"type": "string"}},
"required": ["expression"],
},
},
},
]
formatted_messages, system_message = llm._format_messages_for_anthropic(
[{"role": "user", "content": "Hello"}]
)
params = llm._prepare_completion_params(
formatted_messages, system_message, crewai_tools
)
tools = params["tools"]
# Should have 3 tools: tool_search + 2 regular
assert len(tools) == 3
# First tool should be the bm25 tool search tool
assert tools[0]["type"] == "tool_search_tool_bm25_20251119"
assert tools[0]["name"] == "tool_search_tool_bm25"
assert "input_schema" not in tools[0]
# All regular tools should have defer_loading=True
for t in tools[1:]:
assert t.get("defer_loading") is True, f"Tool {t['name']} missing defer_loading"
def test_tool_search_regex_config():
"""tool_search with regex config should use regex variant."""
from crewai.llms.providers.anthropic.completion import AnthropicToolSearchConfig
config = AnthropicToolSearchConfig(type="regex")
llm = LLM(model="anthropic/claude-sonnet-4-5", tool_search=config)
crewai_tools = [
{
"type": "function",
"function": {
"name": "test_tool",
"description": "A test tool",
"parameters": {
"type": "object",
"properties": {"q": {"type": "string"}},
"required": ["q"],
},
},
},
]
formatted_messages, system_message = llm._format_messages_for_anthropic(
[{"role": "user", "content": "Hello"}]
)
params = llm._prepare_completion_params(
formatted_messages, system_message, crewai_tools
)
tools = params["tools"]
assert tools[0]["type"] == "tool_search_tool_regex_20251119"
assert tools[0]["name"] == "tool_search_tool_regex"
def test_tool_search_disabled_by_default():
"""tool_search=None (default) should NOT inject anything."""
llm = LLM(model="anthropic/claude-sonnet-4-5")
crewai_tools = [
{
"type": "function",
"function": {
"name": "test_tool",
"description": "A test tool",
"parameters": {
"type": "object",
"properties": {"q": {"type": "string"}},
"required": ["q"],
},
},
},
]
formatted_messages, system_message = llm._format_messages_for_anthropic(
[{"role": "user", "content": "Hello"}]
)
params = llm._prepare_completion_params(
formatted_messages, system_message, crewai_tools
)
tools = params["tools"]
assert len(tools) == 1
for t in tools:
assert t.get("type", "") not in (
"tool_search_tool_bm25_20251119",
"tool_search_tool_regex_20251119",
)
assert "defer_loading" not in t
def test_tool_search_no_duplicate_when_manually_provided():
"""If user passes a tool search tool manually, don't inject a duplicate."""
llm = LLM(model="anthropic/claude-sonnet-4-5", tool_search=True)
# User manually includes a tool search tool
tools_with_search = [
{"type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex"},
{
"type": "function",
"function": {
"name": "test_tool",
"description": "A test tool",
"parameters": {
"type": "object",
"properties": {"q": {"type": "string"}},
"required": ["q"],
},
},
},
]
formatted_messages, system_message = llm._format_messages_for_anthropic(
[{"role": "user", "content": "Hello"}]
)
params = llm._prepare_completion_params(
formatted_messages, system_message, tools_with_search
)
tools = params["tools"]
search_tools = [
t for t in tools
if t.get("type", "").startswith("tool_search_tool")
]
# Should only have 1 tool search tool (the user's manual one)
assert len(search_tools) == 1
assert search_tools[0]["type"] == "tool_search_tool_regex_20251119"
def test_tool_search_passthrough_preserves_tool_search_type():
"""_convert_tools_for_interference should pass through tool search tools unchanged."""
llm = LLM(model="anthropic/claude-sonnet-4-5")
tools = [
{"type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex"},
{
"name": "get_weather",
"description": "Get weather",
"input_schema": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
},
},
]
converted = llm._convert_tools_for_interference(tools)
assert len(converted) == 2
# Tool search tool should be passed through exactly
assert converted[0] == {
"type": "tool_search_tool_regex_20251119",
"name": "tool_search_tool_regex",
}
# Regular tool should be preserved
assert converted[1]["name"] == "get_weather"
assert "input_schema" in converted[1]
def test_tool_search_tool_choice_excludes_search_tool():
"""When tool_search is enabled with a single regular tool, tool_choice should still work."""
llm = LLM(model="anthropic/claude-sonnet-4-5", tool_search=True)
crewai_tools = [
{
"type": "function",
"function": {
"name": "test_tool",
"description": "A test tool",
"parameters": {
"type": "object",
"properties": {"q": {"type": "string"}},
"required": ["q"],
},
},
},
]
formatted_messages, system_message = llm._format_messages_for_anthropic(
[{"role": "user", "content": "Hello"}]
)
params = llm._prepare_completion_params(
formatted_messages,
system_message,
crewai_tools,
available_functions={"test_tool": lambda q: "result"},
)
# Should have tool_choice forcing the single regular tool
assert "tool_choice" in params
assert params["tool_choice"]["name"] == "test_tool"
def test_tool_search_via_llm_class():
"""Verify tool_search param passes through LLM class correctly."""
from crewai.llms.providers.anthropic.completion import (
AnthropicCompletion,
AnthropicToolSearchConfig,
)
# Test with True
llm = LLM(model="anthropic/claude-sonnet-4-5", tool_search=True)
assert isinstance(llm, AnthropicCompletion)
assert llm.tool_search is not None
assert llm.tool_search.type == "bm25"
# Test with config
llm2 = LLM(
model="anthropic/claude-sonnet-4-5",
tool_search=AnthropicToolSearchConfig(type="regex"),
)
assert llm2.tool_search is not None
assert llm2.tool_search.type == "regex"
# Test without (default)
llm3 = LLM(model="anthropic/claude-sonnet-4-5")
assert llm3.tool_search is None
# Many tools shared by the VCR tests below
_MANY_TOOLS = [
{
"name": name,
"description": desc,
"input_schema": {
"type": "object",
"properties": {"input": {"type": "string", "description": f"Input for {name}"}},
"required": ["input"],
},
}
for name, desc in [
("get_weather", "Get current weather conditions for a specified location"),
("search_files", "Search through files in the workspace by name or content"),
("read_database", "Read records from a database table with optional filtering"),
("write_database", "Write or update records in a database table"),
("send_email", "Send an email message to one or more recipients"),
("read_email", "Read emails from inbox with filtering options"),
("create_ticket", "Create a new support ticket in the ticketing system"),
("update_ticket", "Update an existing support ticket status or description"),
("list_users", "List all users in the system with optional filters"),
("get_user_profile", "Get detailed profile information for a specific user"),
("deploy_service", "Deploy a service to the specified environment"),
("rollback_service", "Rollback a service deployment to a previous version"),
("get_service_logs", "Get service logs filtered by time range and severity"),
("run_sql_query", "Run a read-only SQL query against the analytics database"),
("create_dashboard", "Create a new monitoring dashboard with widgets"),
]
]
@pytest.mark.vcr()
def test_tool_search_discovers_and_calls_tool():
"""Tool search should discover the right tool and return a tool_use block."""
llm = LLM(model="anthropic/claude-sonnet-4-5", tool_search=True)
result = llm.call(
"What is the weather in Tokyo?",
tools=_MANY_TOOLS,
)
# Should return tool_use blocks (list) since no available_functions provided
assert isinstance(result, list)
assert len(result) >= 1
# The discovered tool should be get_weather
tool_names = [getattr(block, "name", None) for block in result]
assert "get_weather" in tool_names
@pytest.mark.vcr()
def test_tool_search_saves_input_tokens():
"""Tool search with deferred loading should use fewer input tokens than loading all tools."""
# Call WITHOUT tool search — all 15 tools loaded upfront
llm_no_search = LLM(model="anthropic/claude-sonnet-4-5")
llm_no_search.call("What is the weather in Tokyo?", tools=_MANY_TOOLS)
usage_no_search = llm_no_search.get_token_usage_summary()
# Call WITH tool search — tools deferred
llm_search = LLM(model="anthropic/claude-sonnet-4-5", tool_search=True)
llm_search.call("What is the weather in Tokyo?", tools=_MANY_TOOLS)
usage_search = llm_search.get_token_usage_summary()
# Tool search should use fewer input tokens
assert usage_search.prompt_tokens < usage_no_search.prompt_tokens, (
f"Expected tool_search ({usage_search.prompt_tokens}) to use fewer input tokens "
f"than no search ({usage_no_search.prompt_tokens})"
)