diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 1a003727e97..a0da14bcc2b 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -639,15 +639,23 @@ class AnthropicModelInfo(BaseLLMModelInfo): return AnthropicTokenCounter() -def strip_advisor_blocks_from_messages(messages: List[Any]) -> List[Any]: +def strip_advisor_blocks_from_messages( + messages: List[Any], replace_with_text: bool = False +) -> 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. + Remove (or replace) server_tool_use (name='advisor') and advisor_tool_result blocks + from assistant message content. 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. + + Args: + messages: Conversation history to process (mutated in-place). + replace_with_text: When True, replace the advisor exchange with an + text block so the executor retains the semantic + context of what the advisor said. When False (default), strip silently. """ for message in messages: if not isinstance(message, dict) or message.get("role") != "assistant": @@ -655,7 +663,9 @@ def strip_advisor_blocks_from_messages(messages: List[Any]) -> List[Any]: content = message.get("content") if not isinstance(content, list): continue - advisor_ids: set = set() + + # Collect advisor server_tool_use ids and their advice text (for replace mode). + advisor_id_to_text: dict = {} for block in content: if ( isinstance(block, dict) @@ -664,26 +674,65 @@ def strip_advisor_blocks_from_messages(messages: List[Any]) -> List[Any]: ): bid = block.get("id") if bid: - advisor_ids.add(bid) - if not advisor_ids: + advisor_id_to_text[bid] = None # text filled in below + + if not advisor_id_to_text: 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" + + # If replacing, collect the advisor response text from advisor_tool_result blocks. + if replace_with_text: + for block in content: + if ( + isinstance(block, dict) + and block.get("type") == "advisor_tool_result" + and block.get("tool_use_id") in advisor_id_to_text + ): + raw = block.get("content") or "" + text = ( + raw + if isinstance(raw, str) + else next( + ( + b.get("text", "") + for b in raw + if isinstance(b, dict) and b.get("type") == "text" + ), + "", + ) ) - or ( - block.get("type") == "advisor_tool_result" - and block.get("tool_use_id") in advisor_ids - ) - ) + advisor_id_to_text[block["tool_use_id"]] = text + + new_content = [] + for block in content: + if not isinstance(block, dict): + new_content.append(block) + continue + is_advisor_use = ( + block.get("type") == "server_tool_use" + and block.get("name") == "advisor" + and block.get("id") in advisor_id_to_text ) - ] + is_advisor_result = ( + block.get("type") == "advisor_tool_result" + and block.get("tool_use_id") in advisor_id_to_text + ) + if is_advisor_use: + if replace_with_text: + advice = advisor_id_to_text.get(block.get("id")) or "" + if advice: + new_content.append( + { + "type": "text", + "text": f"\n{advice}\n", + } + ) + # else: drop silently + elif is_advisor_result: + pass # always drop — replaced above (or stripped) + else: + new_content.append(block) + + message["content"] = new_content return messages