Merge pull request #25525 from BerriAI/feat/anthropic-advisor-tool

feat(anthropic): support advisor_20260301 tool type
This commit is contained in:
ishaan-berri 2026-04-10 16:39:34 -07:00 committed by GitHub
commit 831083b565
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 788 additions and 30 deletions

View file

@ -0,0 +1,422 @@
# Advisor Tool
Pair a faster executor model with a higher-intelligence advisor model that provides strategic guidance mid-generation.
The advisor tool lets a fast, lower-cost executor model (Sonnet or Haiku) consult a high-intelligence advisor model (Opus 4.6) mid-generation. The advisor reads the full conversation and produces a plan or course correction — typically 400700 text tokens — and the executor continues with the task.
This pattern is well-suited for long-horizon agentic workloads (coding agents, computer use, multi-step research) where most turns are mechanical but having an excellent plan is crucial. You get close to advisor-solo quality while the bulk of token generation happens at executor-model rates.
:::info Beta
The advisor tool is in beta. Include `anthropic-beta: advisor-tool-2026-03-01` in your requests — LiteLLM adds this automatically when it detects the advisor tool in your `tools` array.
:::
## Supported Providers
| Provider | Chat Completions API | Messages API |
|----------|---------------------|--------------|
| **Anthropic API** | ✅ | ✅ |
| **Azure Anthropic** | ❌ (coming soon) | ❌ (coming soon) |
| **Google Cloud Vertex AI** | ❌ (coming soon) | ❌ (coming soon) |
| **Amazon Bedrock** | ❌ (coming soon) | ❌ (coming soon) |
## Model Compatibility
The executor and advisor models must form a valid pair. Currently the only supported advisor model is `claude-opus-4-6`.
| Executor | Advisor |
|----------|---------|
| `claude-haiku-4-5-20251001` | `claude-opus-4-6` |
| `claude-sonnet-4-6` | `claude-opus-4-6` |
| `claude-opus-4-6` | `claude-opus-4-6` |
---
## Chat Completions API
### SDK Usage
#### Basic Example
```python showLineNumbers title="Advisor Tool — litellm.completion()"
import litellm
response = litellm.completion(
model="anthropic/claude-sonnet-4-6",
messages=[
{"role": "user", "content": "Build a concurrent worker pool in Go with graceful shutdown."}
],
tools=[
{
"type": "advisor_20260301",
"name": "advisor",
"model": "claude-opus-4-6",
}
],
max_tokens=4096,
)
print(response.choices[0].message.content)
```
#### With Optional Parameters
```python showLineNumbers title="Advisor Tool with max_uses and caching"
import litellm
response = litellm.completion(
model="anthropic/claude-sonnet-4-6",
messages=[
{"role": "user", "content": "Build a REST API with authentication in Python."}
],
tools=[
{
"type": "advisor_20260301",
"name": "advisor",
"model": "claude-opus-4-6",
"max_uses": 3, # cap advisor calls per request
"caching": {"type": "ephemeral", "ttl": "5m"}, # enable for 3+ calls per conversation
}
],
max_tokens=4096,
)
```
#### Streaming
```python showLineNumbers title="Streaming with Advisor Tool"
import litellm
response = litellm.completion(
model="anthropic/claude-sonnet-4-6",
messages=[
{"role": "user", "content": "Implement a distributed rate limiter."}
],
tools=[
{
"type": "advisor_20260301",
"name": "advisor",
"model": "claude-opus-4-6",
}
],
max_tokens=4096,
stream=True,
)
for chunk in response:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")
```
:::note Streaming behavior
The advisor sub-inference does not stream. The executor's stream pauses while the advisor runs, then the full advisor result arrives in a single event. Executor output resumes streaming afterward.
:::
#### Multi-Turn Conversation
```python showLineNumbers title="Multi-Turn with Advisor Tool"
import litellm
tools = [
{
"type": "advisor_20260301",
"name": "advisor",
"model": "claude-opus-4-6",
}
]
messages = [
{"role": "user", "content": "Build a concurrent worker pool in Go with graceful shutdown."}
]
response = litellm.completion(
model="anthropic/claude-sonnet-4-6",
messages=messages,
tools=tools,
max_tokens=4096,
)
# Append the full response (includes server_tool_use + advisor_tool_result blocks)
messages.append({"role": "assistant", "content": response.choices[0].message.content})
# Continue the conversation — keep the same tools array
messages.append({"role": "user", "content": "Now add a max-in-flight limit of 10."})
response2 = litellm.completion(
model="anthropic/claude-sonnet-4-6",
messages=messages,
tools=tools,
max_tokens=4096,
)
```
:::tip Auto-strip on follow-up turns
LiteLLM automatically strips `advisor_tool_result` blocks from message history when the advisor tool is not present in the current request. This prevents the Anthropic 400 error that would otherwise occur.
:::
### AI Gateway Usage
#### Proxy Configuration
```yaml showLineNumbers title="config.yaml"
model_list:
- model_name: claude-sonnet
litellm_params:
model: anthropic/claude-sonnet-4-6
api_key: os.environ/ANTHROPIC_API_KEY
```
#### Client Request via Proxy
```python showLineNumbers title="Advisor Tool via AI Gateway"
from openai import OpenAI
client = OpenAI(
api_key="your-litellm-proxy-key",
base_url="http://0.0.0.0:4000/v1"
)
response = client.chat.completions.create(
model="claude-sonnet",
messages=[
{"role": "user", "content": "Implement a distributed rate limiter in Python."}
],
tools=[
{
"type": "advisor_20260301",
"name": "advisor",
"model": "claude-opus-4-6",
}
],
max_tokens=4096,
)
```
---
## Messages API
### SDK Usage
#### Basic Example
```python showLineNumbers title="Advisor Tool — litellm.anthropic.messages"
import asyncio
import litellm
async def main():
response = await litellm.anthropic.messages.acreate(
model="anthropic/claude-sonnet-4-6",
messages=[
{"role": "user", "content": "Build a concurrent worker pool in Go with graceful shutdown."}
],
tools=[
{
"type": "advisor_20260301",
"name": "advisor",
"model": "claude-opus-4-6",
}
],
max_tokens=4096,
)
print(response)
asyncio.run(main())
```
#### Streaming
```python showLineNumbers title="Messages API Streaming with Advisor Tool"
import asyncio
import json
import litellm
async def main():
response = await litellm.anthropic.messages.acreate(
model="anthropic/claude-sonnet-4-6",
messages=[
{"role": "user", "content": "Implement a distributed rate limiter."}
],
tools=[
{
"type": "advisor_20260301",
"name": "advisor",
"model": "claude-opus-4-6",
}
],
max_tokens=4096,
stream=True,
)
async for chunk in response:
if isinstance(chunk, bytes):
for line in chunk.decode("utf-8").split("\n"):
if line.startswith("data: "):
try:
print(json.loads(line[6:]))
except json.JSONDecodeError:
pass
asyncio.run(main())
```
### AI Gateway Usage
#### Proxy Configuration
```yaml showLineNumbers title="config.yaml"
model_list:
- model_name: claude-sonnet
litellm_params:
model: anthropic/claude-sonnet-4-6
api_key: os.environ/ANTHROPIC_API_KEY
```
#### Client Request via Proxy (Anthropic SDK)
```python showLineNumbers title="Advisor Tool via AI Gateway (Anthropic SDK)"
import anthropic
client = anthropic.Anthropic(
api_key="your-litellm-proxy-key",
base_url="http://0.0.0.0:4000"
)
response = client.beta.messages.create(
model="claude-sonnet",
max_tokens=4096,
betas=["advisor-tool-2026-03-01"],
messages=[
{"role": "user", "content": "Build a concurrent worker pool in Go with graceful shutdown."}
],
tools=[
{
"type": "advisor_20260301",
"name": "advisor",
"model": "claude-opus-4-6",
}
],
)
print(response)
```
---
## Response Structure
A successful advisor call returns `server_tool_use` and `advisor_tool_result` blocks in the assistant content:
```json title="Response with advisor blocks"
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "Let me consult the advisor on this."
},
{
"type": "server_tool_use",
"id": "srvtoolu_abc123",
"name": "advisor",
"input": {}
},
{
"type": "advisor_tool_result",
"tool_use_id": "srvtoolu_abc123",
"content": {
"type": "advisor_result",
"text": "Use a channel-based coordination pattern. The tricky part is draining in-flight work during shutdown: close the input channel first, then wait on a WaitGroup..."
}
},
{
"type": "text",
"text": "Here's the implementation using a channel-based coordination pattern..."
}
]
}
```
Pass the full assistant content, including advisor blocks, back on subsequent turns. LiteLLM handles this automatically through `provider_specific_fields`.
---
## Cost Control
Advisor calls run as a separate sub-inference billed at the advisor model's rates. Usage is reported in `usage.iterations[]`:
```json title="Usage with advisor sub-inference"
{
"usage": {
"input_tokens": 412,
"output_tokens": 531,
"iterations": [
{
"type": "message",
"input_tokens": 412,
"output_tokens": 89
},
{
"type": "advisor_message",
"model": "claude-opus-4-6",
"input_tokens": 823,
"output_tokens": 1612
},
{
"type": "message",
"input_tokens": 1348,
"output_tokens": 442
}
]
}
}
```
Top-level `usage` reflects executor tokens only. Advisor tokens appear in `iterations` entries with `type: "advisor_message"` and are billed at Opus rates.
**Tips:**
- Enable `caching` on the tool definition only when you expect 3+ advisor calls per conversation; it costs more than it saves below that threshold.
- Use `max_uses` to cap advisor calls per request. Once reached, the executor continues without further advice.
- For conversation-level caps, count advisor calls client-side. When you reach your limit, remove the advisor tool from `tools`.
---
## Recommended System Prompt
For coding and agent tasks, Anthropic recommends prepending these blocks to your system prompt for consistent advisor timing and optimal cost/quality:
```text title="Timing guidance (prepend to system prompt)"
You have access to an `advisor` tool backed by a stronger reviewer model. It takes NO parameters — when you call advisor(), your entire conversation history is automatically forwarded. They see the task, every tool call you've made, every result you've seen.
Call advisor BEFORE substantive work — before writing, before committing to an interpretation, before building on an assumption. If the task requires orientation first (finding files, fetching a source, seeing what's there), do that, then call advisor. Orientation is not substantive work. Writing, editing, and declaring an answer are.
Also call advisor:
- When you believe the task is complete. BEFORE this call, make your deliverable durable: write the file, save the result, commit the change.
- When stuck — errors recurring, approach not converging, results that don't fit.
- When considering a change of approach.
On tasks longer than a few steps, call advisor at least once before committing to an approach and once before declaring done. On short reactive tasks where the next action is dictated by tool output you just read, you don't need to keep calling.
```
```text title="Advice weight guidance (add after timing block)"
Give the advice serious weight. If you follow a step and it fails empirically, or you have primary-source evidence that contradicts a specific claim, adapt. A passing self-test is not evidence the advice is wrong.
If you've already retrieved data pointing one way and the advisor points another: don't silently switch. Surface the conflict in one more advisor call — "I found X, you suggest Y, which constraint breaks the tie?"
```
To reduce advisor output length by 3545% without losing quality, add:
```text title="Cost reduction (optional, add before timing block)"
The advisor should respond in under 100 words and use enumerated steps, not explanations.
```
---
## Additional Resources
- [Anthropic Advisor Tool Documentation](https://platform.claude.com/docs/en/agents-and-tools/tool-use/advisor-tool)
- [LiteLLM Tool Calling Guide](https://docs.litellm.ai/docs/completion/function_call)

View file

@ -861,6 +861,7 @@ const sidebars = {
]
},
"providers/anthropic",
"providers/anthropic_tool_search",
"providers/aws_sagemaker",
{
type: "category",
@ -1232,6 +1233,7 @@ const learnSidebar = {
"completion/web_fetch",
"completion/computer_use",
"guides/code_interpreter",
"completion/anthropic_advisor_tool",
"completion/message_sanitization",
],
},

View file

@ -1,6 +1,7 @@
{
"description": "Mapping of Anthropic beta headers for each provider. Keys are input header names, values are provider-specific header names (or null if unsupported). Only headers present in mapping keys with non-null values can be forwarded.",
"anthropic": {
"advisor-tool-2026-03-01": "advisor-tool-2026-03-01",
"advanced-tool-use-2025-11-20": "advanced-tool-use-2025-11-20",
"bash_20241022": null,
"bash_20250124": null,
@ -31,6 +32,7 @@
"web-search-2025-03-05": "web-search-2025-03-05"
},
"azure_ai": {
"advisor-tool-2026-03-01": null,
"advanced-tool-use-2025-11-20": "advanced-tool-use-2025-11-20",
"bash_20241022": null,
"bash_20250124": null,
@ -60,6 +62,7 @@
"web-search-2025-03-05": "web-search-2025-03-05"
},
"bedrock_converse": {
"advisor-tool-2026-03-01": null,
"advanced-tool-use-2025-11-20": null,
"bash_20241022": null,
"bash_20250124": null,
@ -90,6 +93,7 @@
"web-search-2025-03-05": null
},
"bedrock": {
"advisor-tool-2026-03-01": null,
"advanced-tool-use-2025-11-20": "tool-search-tool-2025-10-19",
"bash_20241022": null,
"bash_20250124": null,
@ -120,6 +124,7 @@
"web-search-2025-03-05": null
},
"vertex_ai": {
"advisor-tool-2026-03-01": null,
"advanced-tool-use-2025-11-20": "tool-search-tool-2025-10-19",
"bash_20241022": null,
"bash_20250124": null,
@ -150,6 +155,7 @@
"web-search-2025-03-05": "web-search-2025-03-05"
},
"databricks": {
"advisor-tool-2026-03-01": null,
"advanced-tool-use-2025-11-20": "advanced-tool-use-2025-11-20",
"bash_20241022": null,
"bash_20250124": null,

View file

@ -19,6 +19,7 @@ from litellm.litellm_core_utils.core_helpers import map_finish_reason
from litellm.llms.base_llm.base_utils import type_to_response_format_param
from litellm.llms.base_llm.chat.transformation import BaseConfig, BaseLLMException
from litellm.types.llms.anthropic import (
ANTHROPIC_ADVISOR_TOOL_TYPE,
ANTHROPIC_BETA_HEADER_VALUES,
ANTHROPIC_HOSTED_TOOLS,
AllAnthropicMessageValues,
@ -75,7 +76,12 @@ from litellm.utils import (
token_counter,
)
from ..common_utils import AnthropicError, AnthropicModelInfo, process_anthropic_headers
from ..common_utils import (
AnthropicError,
AnthropicModelInfo,
process_anthropic_headers,
strip_advisor_blocks_from_messages,
)
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
@ -508,6 +514,23 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
type="tool_search_tool_bm25_20251119",
name=tool_name,
)
elif tool["type"] == ANTHROPIC_ADVISOR_TOOL_TYPE:
from litellm.types.llms.anthropic import AnthropicAdvisorTool
_tool_dict = cast(dict, tool)
advisor_model = _tool_dict.get("model")
if not isinstance(advisor_model, str):
raise ValueError("Advisor tool must have a valid model")
_advisor_tool = AnthropicAdvisorTool(
type=ANTHROPIC_ADVISOR_TOOL_TYPE,
name="advisor",
model=advisor_model,
)
if _tool_dict.get("max_uses") is not None:
_advisor_tool["max_uses"] = _tool_dict["max_uses"]
if _tool_dict.get("caching") is not None:
_advisor_tool["caching"] = _tool_dict["caching"]
returned_tool = _advisor_tool # type: ignore[assignment]
if returned_tool is None and mcp_server is None:
raise ValueError(f"Unsupported tool type: {tool['type']}")
@ -964,11 +987,11 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
if mcp_servers:
optional_params["mcp_servers"] = mcp_servers
elif param == "tool_choice" or param == "parallel_tool_calls":
_tool_choice: Optional[
AnthropicMessagesToolChoice
] = self._map_tool_choice(
tool_choice=non_default_params.get("tool_choice"),
parallel_tool_use=non_default_params.get("parallel_tool_calls"),
_tool_choice: Optional[AnthropicMessagesToolChoice] = (
self._map_tool_choice(
tool_choice=non_default_params.get("tool_choice"),
parallel_tool_use=non_default_params.get("parallel_tool_calls"),
)
)
if _tool_choice is not None:
@ -1066,9 +1089,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
self.map_openai_context_management_to_anthropic(value)
)
if anthropic_context_management is not None:
optional_params[
"context_management"
] = anthropic_context_management
optional_params["context_management"] = (
anthropic_context_management
)
elif param == "speed" and isinstance(value, str):
# Pass through Anthropic-specific speed parameter for fast mode
optional_params["speed"] = value
@ -1142,9 +1165,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
text=system_message_block["content"],
)
if "cache_control" in system_message_block:
anthropic_system_message_content[
"cache_control"
] = system_message_block["cache_control"]
anthropic_system_message_content["cache_control"] = (
system_message_block["cache_control"]
)
anthropic_system_message_list.append(
anthropic_system_message_content
)
@ -1168,9 +1191,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
)
)
if "cache_control" in _content:
anthropic_system_message_content[
"cache_control"
] = _content["cache_control"]
anthropic_system_message_content["cache_control"] = (
_content["cache_control"]
)
anthropic_system_message_list.append(
anthropic_system_message_content
@ -1311,6 +1334,12 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
self._ensure_beta_header(
headers, ANTHROPIC_BETA_HEADER_VALUES.FAST_MODE_2026_02_01.value
)
for tool in _tools:
if tool.get("type") == ANTHROPIC_ADVISOR_TOOL_TYPE:
self._ensure_beta_header(
headers, ANTHROPIC_BETA_HEADER_VALUES.ADVISOR_TOOL_2026_03_01.value
)
break
return headers
def transform_request(
@ -1390,6 +1419,16 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
message="{}\nReceived Messages={}".format(str(e), messages),
) # don't use verbose_logger.exception, if exception is raised
## Auto-strip advisor blocks from history if advisor tool is absent.
## Prevents Anthropic 400: advisor_tool_result in history requires advisor tool.
_all_tools = optional_params.get("tools") or []
_has_advisor = any(
isinstance(t, dict) and t.get("type") == ANTHROPIC_ADVISOR_TOOL_TYPE
for t in _all_tools
)
if not _has_advisor:
anthropic_messages = strip_advisor_blocks_from_messages(anthropic_messages)
## Add code_execution tool if container_upload is in messages
_tools = (
cast(
@ -1477,9 +1516,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
)
return _message
def extract_response_content(
self, completion_response: dict
) -> Tuple[
def extract_response_content(self, completion_response: dict) -> Tuple[
str,
Optional[List[Any]],
Optional[
@ -1773,9 +1810,9 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
code_interpreter_results = self._build_code_interpreter_results(
tool_results, code_by_id, container_id
)
provider_specific_fields[
"code_interpreter_results"
] = code_interpreter_results
provider_specific_fields["code_interpreter_results"] = (
code_interpreter_results
)
container = completion_response.get("container")
if container is not None:

View file

@ -2,7 +2,7 @@
This file contains common utils for anthropic calls.
"""
from typing import Dict, List, Optional, Union
from typing import Any, Dict, List, Optional, Union
import httpx
@ -464,9 +464,9 @@ class AnthropicModelInfo(BaseLLMModelInfo):
if web_search_tool_used:
from litellm.types.llms.anthropic import ANTHROPIC_BETA_HEADER_VALUES
headers[
"anthropic-beta"
] = ANTHROPIC_BETA_HEADER_VALUES.WEB_SEARCH_2025_03_05.value
headers["anthropic-beta"] = (
ANTHROPIC_BETA_HEADER_VALUES.WEB_SEARCH_2025_03_05.value
)
elif len(betas) > 0:
headers["anthropic-beta"] = ",".join(betas)
@ -639,6 +639,54 @@ class AnthropicModelInfo(BaseLLMModelInfo):
return AnthropicTokenCounter()
def strip_advisor_blocks_from_messages(messages: List[Any]) -> List[Any]:
"""
Remove server_tool_use (name='advisor') and advisor_tool_result blocks from
assistant message content when the advisor tool is absent from the request.
Prevents Anthropic 400 invalid_request_error: if advisor_tool_result blocks
exist in history but the advisor tool is not in the tools array, the API rejects
the request. This happens when the user has removed the advisor tool for cost
control or on a follow-up turn.
"""
for message in messages:
if message.get("role") != "assistant":
continue
content = message.get("content")
if not isinstance(content, list):
continue
advisor_ids: set = set()
for block in content:
if (
isinstance(block, dict)
and block.get("type") == "server_tool_use"
and block.get("name") == "advisor"
):
bid = block.get("id")
if bid:
advisor_ids.add(bid)
if not advisor_ids:
continue
message["content"] = [
block
for block in content
if not (
isinstance(block, dict)
and (
(
block.get("type") == "server_tool_use"
and block.get("name") == "advisor"
)
or (
block.get("type") == "advisor_tool_result"
and block.get("tool_use_id") in advisor_ids
)
)
)
]
return messages
def process_anthropic_headers(headers: Union[httpx.Headers, dict]) -> dict:
openai_headers = {}
if "anthropic-ratelimit-requests-limit" in headers:

View file

@ -8,6 +8,7 @@ from litellm.llms.base_llm.anthropic_messages.transformation import (
BaseAnthropicMessagesConfig,
)
from litellm.types.llms.anthropic import (
ANTHROPIC_ADVISOR_TOOL_TYPE,
ANTHROPIC_BETA_HEADER_VALUES,
AnthropicMessagesRequest,
)
@ -21,6 +22,7 @@ from ...common_utils import (
AnthropicError,
AnthropicModelInfo,
optionally_handle_anthropic_oauth,
strip_advisor_blocks_from_messages,
)
DEFAULT_ANTHROPIC_API_VERSION = "2023-06-01"
@ -208,12 +210,23 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
)
)
if transformed_context_management is not None:
anthropic_messages_optional_request_params[
"context_management"
] = transformed_context_management
anthropic_messages_optional_request_params["context_management"] = (
transformed_context_management
)
####### get required params for all anthropic messages requests ######
verbose_logger.debug(f"TRANSFORMATION DEBUG - Messages: {messages}")
# Auto-strip advisor blocks from history if advisor tool is absent.
# Prevents Anthropic 400: advisor_tool_result in history requires advisor tool.
_tools = anthropic_messages_optional_request_params.get("tools") or []
_has_advisor = any(
isinstance(t, dict) and t.get("type") == ANTHROPIC_ADVISOR_TOOL_TYPE
for t in _tools
)
if not _has_advisor:
messages = strip_advisor_blocks_from_messages(messages) # type: ignore[assignment]
anthropic_messages_request: AnthropicMessagesRequest = AnthropicMessagesRequest(
messages=messages,
max_tokens=max_tokens,
@ -324,6 +337,19 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
if optional_params.get("speed") == "fast":
beta_values.add(ANTHROPIC_BETA_HEADER_VALUES.FAST_MODE_2026_02_01.value)
# Check for advisor tool
tools = optional_params.get("tools")
if tools:
for tool in tools:
if (
isinstance(tool, dict)
and tool.get("type") == ANTHROPIC_ADVISOR_TOOL_TYPE
):
beta_values.add(
ANTHROPIC_BETA_HEADER_VALUES.ADVISOR_TOOL_2026_03_01.value
)
break
# Check for tool search tools
tools = optional_params.get("tools")
if tools:

View file

@ -126,6 +126,19 @@ class AnthropicToolSearchToolBM25(TypedDict, total=False):
input_examples: Optional[List[Dict[str, Any]]]
ANTHROPIC_ADVISOR_TOOL_TYPE = "advisor_20260301"
class AnthropicAdvisorTool(TypedDict, total=False):
"""Advisor tool — pairs a fast executor model with a high-intelligence advisor model."""
type: Required[Literal["advisor_20260301"]]
name: Required[Literal["advisor"]]
model: Required[str]
max_uses: Optional[int]
caching: Optional[dict]
class ToolReference(TypedDict, total=False):
"""Reference to a tool that should be expanded from deferred tools."""
@ -165,6 +178,7 @@ AllAnthropicToolsValues = Union[
AnthropicMemoryTool,
AnthropicToolSearchToolRegex,
AnthropicToolSearchToolBM25,
AnthropicAdvisorTool,
]
@ -654,6 +668,7 @@ class ANTHROPIC_BETA_HEADER_VALUES(str, Enum):
STRUCTURED_OUTPUT_2025_09_25 = "structured-outputs-2025-11-13"
ADVANCED_TOOL_USE_2025_11_20 = "advanced-tool-use-2025-11-20"
FAST_MODE_2026_02_01 = "fast-mode-2026-02-01"
ADVISOR_TOOL_2026_03_01 = "advisor-tool-2026-03-01"
# Tool search beta header constant (for Anthropic direct API and Microsoft Foundry)

View file

@ -12,6 +12,7 @@ from litellm.llms.anthropic.chat.transformation import AnthropicConfig
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
AnthropicMessagesConfig,
)
from litellm.types.llms.anthropic import ANTHROPIC_BETA_HEADER_VALUES
from litellm.types.utils import ServerToolUse
@ -3367,7 +3368,9 @@ def test_extract_response_content_thinking_block_null_thinking():
text, _, thinking_blocks, _, _, _, _, _ = config.extract_response_content(
completion_response_null
)
assert thinking_blocks is not None, "thinking blocks should not be None when thinking=null"
assert (
thinking_blocks is not None
), "thinking blocks should not be None when thinking=null"
assert len(thinking_blocks) == 1
assert "Hello" in text
@ -3381,7 +3384,9 @@ def test_extract_response_content_thinking_block_null_thinking():
text, _, thinking_blocks, _, _, _, _, _ = config.extract_response_content(
completion_response_missing
)
assert thinking_blocks is not None, "thinking blocks should not be None when thinking key is absent"
assert (
thinking_blocks is not None
), "thinking blocks should not be None when thinking key is absent"
assert len(thinking_blocks) == 1
assert "World" in text
@ -3399,3 +3404,200 @@ def test_extract_response_content_thinking_block_null_thinking():
assert len(thinking_blocks) == 1
assert thinking_blocks[0]["thinking"] == "Let me think..."
assert "Done" in text
def test_advisor_tool_map_tool_helper():
"""advisor_20260301 tool type should not raise ValueError."""
config = AnthropicConfig()
tool = {
"type": "advisor_20260301",
"name": "advisor",
"model": "claude-opus-4-6",
}
returned_tool, mcp_server = config._map_tool_helper(tool) # type: ignore
assert returned_tool is not None
assert returned_tool["type"] == "advisor_20260301"
assert returned_tool["model"] == "claude-opus-4-6"
assert mcp_server is None
def test_advisor_tool_map_tool_helper_with_optional_fields():
"""advisor_20260301 tool with max_uses and caching should be mapped correctly."""
config = AnthropicConfig()
tool = {
"type": "advisor_20260301",
"name": "advisor",
"model": "claude-opus-4-6",
"max_uses": 3,
"caching": {"type": "ephemeral", "ttl": "5m"},
}
returned_tool, _ = config._map_tool_helper(tool) # type: ignore
assert returned_tool is not None
assert returned_tool["max_uses"] == 3
assert returned_tool["caching"] == {"type": "ephemeral", "ttl": "5m"}
def test_advisor_tool_map_tool_helper_missing_model():
"""advisor_20260301 without model should raise ValueError."""
config = AnthropicConfig()
tool = {"type": "advisor_20260301", "name": "advisor"}
with pytest.raises(ValueError, match="valid model"):
config._map_tool_helper(tool) # type: ignore
def test_advisor_beta_header_injected():
"""advisor-tool-2026-03-01 beta header is auto-injected when advisor tool is present."""
config = AnthropicConfig()
headers: dict = {}
optional_params = {
"tools": [
{
"type": "advisor_20260301",
"name": "advisor",
"model": "claude-opus-4-6",
}
]
}
result = config.update_headers_with_optional_anthropic_beta(
headers, optional_params
)
assert ANTHROPIC_BETA_HEADER_VALUES.ADVISOR_TOOL_2026_03_01.value in result.get(
"anthropic-beta", ""
)
def test_advisor_beta_header_not_injected_without_tool():
"""advisor-tool-2026-03-01 beta header is NOT added when advisor tool is absent."""
config = AnthropicConfig()
headers: dict = {}
optional_params: dict = {"tools": []}
result = config.update_headers_with_optional_anthropic_beta(
headers, optional_params
)
assert "advisor-tool-2026-03-01" not in result.get("anthropic-beta", "")
def test_advisor_tool_result_preserved_in_response():
"""advisor_tool_result blocks are preserved in tool_results (not dropped)."""
config = AnthropicConfig()
completion_response = {
"content": [
{"type": "text", "text": "Consulting advisor."},
{
"type": "server_tool_use",
"id": "srvtoolu_abc123",
"name": "advisor",
"input": {},
},
{
"type": "advisor_tool_result",
"tool_use_id": "srvtoolu_abc123",
"content": {
"type": "advisor_result",
"text": "Use a channel-based pattern.",
},
},
{"type": "text", "text": "Here is the implementation."},
]
}
text, _, _, _, tool_calls, _, tool_results, _ = config.extract_response_content(
completion_response
)
assert "Consulting advisor." in text
assert "Here is the implementation." in text
# server_tool_use (advisor) should be a tool_call
assert len(tool_calls) == 1
assert tool_calls[0]["function"]["name"] == "advisor"
assert tool_calls[0]["id"] == "srvtoolu_abc123"
# advisor_tool_result should be in tool_results
assert tool_results is not None
assert len(tool_results) == 1
assert tool_results[0]["type"] == "advisor_tool_result"
assert tool_results[0]["tool_use_id"] == "srvtoolu_abc123"
def test_messages_path_advisor_beta_header_injected():
"""advisor-tool-2026-03-01 beta header is auto-injected in /messages path."""
config = AnthropicMessagesConfig()
headers: dict = {}
optional_params = {
"tools": [
{
"type": "advisor_20260301",
"name": "advisor",
"model": "claude-opus-4-6",
}
]
}
result = config._update_headers_with_anthropic_beta(headers, optional_params)
assert "advisor-tool-2026-03-01" in result.get("anthropic-beta", "")
def test_messages_path_advisor_beta_header_preserved_when_user_sends_it():
"""Existing anthropic-beta headers are preserved and advisor header is merged."""
config = AnthropicMessagesConfig()
headers: dict = {"anthropic-beta": "advisor-tool-2026-03-01"}
optional_params: dict = {"tools": []}
result = config._update_headers_with_anthropic_beta(headers, optional_params)
assert "advisor-tool-2026-03-01" in result.get("anthropic-beta", "")
def test_strip_advisor_blocks_when_no_advisor_tool():
"""
Auto-strip removes server_tool_use(advisor) + advisor_tool_result blocks when
advisor tool is absent, preventing Anthropic 400 on follow-up turns.
"""
from litellm.llms.anthropic.common_utils import strip_advisor_blocks_from_messages
messages = [
{"role": "user", "content": "Build a worker pool."},
{
"role": "assistant",
"content": [
{"type": "text", "text": "Let me consult the advisor."},
{
"type": "server_tool_use",
"id": "srvtoolu_abc123",
"name": "advisor",
"input": {},
},
{
"type": "advisor_tool_result",
"tool_use_id": "srvtoolu_abc123",
"content": {"type": "advisor_result", "text": "Use channels."},
},
{"type": "text", "text": "Here is the implementation."},
],
},
]
result = strip_advisor_blocks_from_messages(messages)
assistant_content = result[1]["content"]
types = [b["type"] for b in assistant_content]
assert "server_tool_use" not in types
assert "advisor_tool_result" not in types
assert "text" in types
assert len(assistant_content) == 2
def test_strip_advisor_blocks_no_op_when_no_advisor_blocks():
"""strip_advisor_blocks_from_messages is a no-op when no advisor blocks exist."""
from litellm.llms.anthropic.common_utils import strip_advisor_blocks_from_messages
messages = [
{"role": "user", "content": "Hello"},
{
"role": "assistant",
"content": [
{"type": "text", "text": "Hi there"},
{
"type": "tool_use",
"id": "toolu_abc",
"name": "get_weather",
"input": {"location": "SF"},
},
],
},
]
original_content = [dict(b) for b in messages[1]["content"]]
result = strip_advisor_blocks_from_messages(messages)
assert result[1]["content"] == original_content