diff --git a/docs/my-website/docs/completion/anthropic_advisor_tool.md b/docs/my-website/docs/completion/anthropic_advisor_tool.md new file mode 100644 index 00000000000..3cb87ffdf3d --- /dev/null +++ b/docs/my-website/docs/completion/anthropic_advisor_tool.md @@ -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 400–700 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 35–45% 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) diff --git a/docs/my-website/sidebars.js b/docs/my-website/sidebars.js index 54581dceb95..b1ec952544b 100644 --- a/docs/my-website/sidebars.js +++ b/docs/my-website/sidebars.js @@ -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", ], }, diff --git a/litellm/anthropic_beta_headers_config.json b/litellm/anthropic_beta_headers_config.json index df8d49ac8f2..7dd5975b7bb 100644 --- a/litellm/anthropic_beta_headers_config.json +++ b/litellm/anthropic_beta_headers_config.json @@ -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, diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 9a99f9efc82..e78c72c13d0 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -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: diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 7d2d0a74961..1205d1afbcf 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -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: diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index 9b60a58260b..46af1f7fbd1 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -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: diff --git a/litellm/types/llms/anthropic.py b/litellm/types/llms/anthropic.py index 37044c2b4f5..c76f27bdd6c 100644 --- a/litellm/types/llms/anthropic.py +++ b/litellm/types/llms/anthropic.py @@ -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) diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py index 10a3c107367..6b1b9ced245 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -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