diff --git a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py index 38f280eef03..72bc43ba938 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py @@ -18,6 +18,7 @@ from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation ) from litellm.llms.bedrock.common_utils import ( apply_bedrock_invoke_structured_output, + bedrock_supports_tool_search, get_anthropic_beta_from_headers, normalize_bedrock_opus_output_config_effort, normalize_custom_field_on_tools, @@ -265,7 +266,7 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): if tool_search_used and not (programmatic_tool_calling_used or input_examples_used): beta_set.discard(ANTHROPIC_TOOL_SEARCH_BETA_HEADER) - if "opus-4" in model.lower() or "opus_4" in model.lower(): + if bedrock_supports_tool_search(model): beta_set.add("tool-search-tool-2025-10-19") auto_beta_list: Final = filter_and_transform_beta_headers( diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index 20c8258e440..4f030b156e7 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -902,6 +902,20 @@ def bedrock_model_accepts_cache_points(model: str | None) -> bool: return any(entry.get("supports_prompt_caching") is True for entry in entries) +def bedrock_supports_tool_search(model: str) -> bool: + """ + Whether Bedrock InvokeModel admits the ``tool_search_tool_*`` tool types on ``model``. + + Backed by the ``supports_tool_search`` flag in ``model_prices_and_context_window.json``, + an exact entry or the ``claude-tool-search`` fallback rule for Claude 4.5 and newer, so a + newly released Claude carries the flag with no code change. An explicit ``false`` on the + resolved entry wins over the rule. + """ + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + return AnthropicModelInfo._supports_model_capability(model, "supports_tool_search", "bedrock") + + def is_claude_4_5_on_bedrock(model: str) -> bool: """ Check if the model supports Bedrock prompt caching with an extended '1h' TTL diff --git a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py index a715d150b4c..4aa2afdbc78 100644 --- a/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/messages/invoke_transformations/anthropic_claude3_transformation.py @@ -31,6 +31,7 @@ from litellm.llms.bedrock.chat.invoke_transformations.base_invoke_transformation from litellm.llms.bedrock.common_utils import ( BedrockError, apply_bedrock_invoke_structured_output, + bedrock_supports_tool_search, ensure_bedrock_anthropic_messages_tool_names, get_anthropic_beta_from_headers, is_claude_4_5_on_bedrock, @@ -386,9 +387,10 @@ class AmazonAnthropicClaudeMessagesConfig( """ Check if the model supports tool search on Bedrock. - The model map's ``supports_tool_search`` flag is authoritative when - ``model`` resolves to an entry that sets it; the name patterns below - cover ids the map cannot resolve (ARNs, unlisted regional variants). + The model map's ``supports_tool_search`` flag is authoritative: an exact + entry, or the ``claude-tool-search`` fallback rule (Claude 4.5 and newer) + for ids the map cannot resolve (ARNs, unlisted regional variants) and for + mapped entries that carry no opinion. Ref: https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool @@ -398,46 +400,7 @@ class AmazonAnthropicClaudeMessagesConfig( Returns: True if the model supports tool search on Bedrock """ - catalog: Final = AnthropicModelInfo._get_provider_resolved_capability(model, "supports_tool_search", "bedrock") - if catalog is not None: - return catalog - - model_lower: Final = model.lower() - - supported_patterns: Final = [ - # Opus 4.5 - "opus-4.5", - "opus_4.5", - "opus-4-5", - "opus_4_5", - # Sonnet 4.5 - "sonnet-4.5", - "sonnet_4.5", - "sonnet-4-5", - "sonnet_4_5", - # Opus 4.6 - "opus-4.6", - "opus_4.6", - "opus-4-6", - "opus_4_6", - # sonnet 4.6 - "sonnet-4.6", - "sonnet_4.6", - "sonnet-4-6", - "sonnet_4_6", - # Opus 4.7 - "opus-4.7", - "opus_4.7", - "opus-4-7", - "opus_4_7", - # Haiku 4.5 - "haiku-4.5", - "haiku_4.5", - "haiku-4-5", - "haiku_4_5", - ] - - return any(pattern in model_lower for pattern in supported_patterns) + return bedrock_supports_tool_search(model) def _get_tool_search_beta_header_for_bedrock( self, @@ -453,7 +416,8 @@ class AmazonAnthropicClaudeMessagesConfig( Bedrock requires a different beta header for tool search than the Anthropic API when tool search is used without programmatic tool calling or input examples: `tool-search-tool-2025-10-19`, and only on - the models listed in `_supports_tool_search_on_bedrock`. + the models the model map flags as `supports_tool_search` + (`_supports_tool_search_on_bedrock`). Ref: https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 602c3ec7037..0826f05cb11 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -1810,6 +1810,7 @@ "cache_read_input_token_cost": 5e-07, "input_cost_per_token": 5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -1847,6 +1848,7 @@ "cache_read_input_token_cost": 5e-07, "input_cost_per_token": 5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -1884,6 +1886,7 @@ "cache_read_input_token_cost": 5.5e-07, "input_cost_per_token": 5.5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -1921,6 +1924,7 @@ "cache_read_input_token_cost": 5.5e-07, "input_cost_per_token": 5.5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -1957,6 +1961,7 @@ "cache_read_input_token_cost": 5.5e-07, "input_cost_per_token": 5.5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -1993,6 +1998,7 @@ "cache_read_input_token_cost": 5.5e-07, "input_cost_per_token": 5.5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2029,6 +2035,7 @@ "cache_read_input_token_cost": 5e-07, "input_cost_per_token": 5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2067,6 +2074,7 @@ "cache_read_input_token_cost": 5e-07, "input_cost_per_token": 5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2105,6 +2113,7 @@ "cache_read_input_token_cost": 5.5e-07, "input_cost_per_token": 5.5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2143,6 +2152,7 @@ "cache_read_input_token_cost": 5.5e-07, "input_cost_per_token": 5.5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2180,6 +2190,7 @@ "cache_read_input_token_cost": 5.5e-07, "input_cost_per_token": 5.5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2217,6 +2228,7 @@ "cache_read_input_token_cost": 5.5e-07, "input_cost_per_token": 5.5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2287,6 +2299,7 @@ "cache_read_input_token_cost": 2e-07, "input_cost_per_token": 2e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2325,6 +2338,7 @@ "cache_read_input_token_cost": 2e-07, "input_cost_per_token": 2e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2363,6 +2377,7 @@ "cache_read_input_token_cost": 2.2e-07, "input_cost_per_token": 2.2e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2401,6 +2416,7 @@ "cache_read_input_token_cost": 2.2e-07, "input_cost_per_token": 2.2e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2438,6 +2454,7 @@ "cache_read_input_token_cost": 2.2e-07, "input_cost_per_token": 2.2e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2475,6 +2492,7 @@ "cache_read_input_token_cost": 2.2e-07, "input_cost_per_token": 2.2e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -46130,6 +46148,7 @@ "cache_read_input_token_cost": 2.4e-07, "input_cost_per_token": 2.4e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -46163,6 +46182,7 @@ "cache_read_input_token_cost": 6e-07, "input_cost_per_token": 6e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -46195,6 +46215,7 @@ "cache_read_input_token_cost": 6e-07, "input_cost_per_token": 6e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -59505,6 +59526,15 @@ "supports_mid_conversation_system": true } }, + { + "name": "claude-tool-search", + "pattern": "claude-[a-z]+-(?:4[-._](?:[5-9]|[1-9]\\d)(?!\\d)|[5-9](?!\\d)(?:[-._]\\d{1,2}(?!\\d))?)", + "fill_missing_for_providers": ["anthropic", "bedrock", "bedrock_converse", "vertex_ai-anthropic_models"], + "description": "Claude at version 4.5 or higher, in any id shape that contains claude--: minors 4.5 through 4.99, any later major-minor, and bare 5+ majors so a new family like claude-fable-5 matches. Two-digit majors are deliberately not matched so ids like claude-opus-41 (4.1) are not read as major 41. Anthropic's tool search docs list every Claude 4.5 and newer model as supported and Opus 4.1 and earlier as unsupported, so the flag follows the version instead of a per-model list. azure_ai is left out on purpose: Anthropic documents tool search as unavailable on Azure-hosted Foundry deployments, and the azure_ai/ key cannot tell those from Anthropic-hosted ones.", + "model_info": { + "supports_tool_search": true + } + }, { "name": "wandb-reasoning-baseline", "pattern": "^wandb/", @@ -63253,6 +63283,7 @@ "cache_read_input_token_cost": 2.4e-07, "input_cost_per_token": 2.4e-06, "litellm_provider": "bedrock", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -63285,6 +63316,7 @@ "cache_read_input_token_cost": 6e-07, "input_cost_per_token": 6e-06, "litellm_provider": "bedrock", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -63316,6 +63348,7 @@ "cache_read_input_token_cost": 6e-07, "input_cost_per_token": 6e-06, "litellm_provider": "bedrock", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -63456,6 +63489,7 @@ "cache_read_input_token_cost": 2.4e-07, "input_cost_per_token": 2.4e-06, "litellm_provider": "bedrock", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -63488,6 +63522,7 @@ "cache_read_input_token_cost": 6e-07, "input_cost_per_token": 6e-06, "litellm_provider": "bedrock", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -63519,6 +63554,7 @@ "cache_read_input_token_cost": 6e-07, "input_cost_per_token": 6e-06, "litellm_provider": "bedrock", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 602c3ec7037..0826f05cb11 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -1810,6 +1810,7 @@ "cache_read_input_token_cost": 5e-07, "input_cost_per_token": 5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -1847,6 +1848,7 @@ "cache_read_input_token_cost": 5e-07, "input_cost_per_token": 5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -1884,6 +1886,7 @@ "cache_read_input_token_cost": 5.5e-07, "input_cost_per_token": 5.5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -1921,6 +1924,7 @@ "cache_read_input_token_cost": 5.5e-07, "input_cost_per_token": 5.5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -1957,6 +1961,7 @@ "cache_read_input_token_cost": 5.5e-07, "input_cost_per_token": 5.5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -1993,6 +1998,7 @@ "cache_read_input_token_cost": 5.5e-07, "input_cost_per_token": 5.5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2029,6 +2035,7 @@ "cache_read_input_token_cost": 5e-07, "input_cost_per_token": 5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2067,6 +2074,7 @@ "cache_read_input_token_cost": 5e-07, "input_cost_per_token": 5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2105,6 +2113,7 @@ "cache_read_input_token_cost": 5.5e-07, "input_cost_per_token": 5.5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2143,6 +2152,7 @@ "cache_read_input_token_cost": 5.5e-07, "input_cost_per_token": 5.5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2180,6 +2190,7 @@ "cache_read_input_token_cost": 5.5e-07, "input_cost_per_token": 5.5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2217,6 +2228,7 @@ "cache_read_input_token_cost": 5.5e-07, "input_cost_per_token": 5.5e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2287,6 +2299,7 @@ "cache_read_input_token_cost": 2e-07, "input_cost_per_token": 2e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2325,6 +2338,7 @@ "cache_read_input_token_cost": 2e-07, "input_cost_per_token": 2e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2363,6 +2377,7 @@ "cache_read_input_token_cost": 2.2e-07, "input_cost_per_token": 2.2e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2401,6 +2416,7 @@ "cache_read_input_token_cost": 2.2e-07, "input_cost_per_token": 2.2e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2438,6 +2454,7 @@ "cache_read_input_token_cost": 2.2e-07, "input_cost_per_token": 2.2e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -2475,6 +2492,7 @@ "cache_read_input_token_cost": 2.2e-07, "input_cost_per_token": 2.2e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -46130,6 +46148,7 @@ "cache_read_input_token_cost": 2.4e-07, "input_cost_per_token": 2.4e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -46163,6 +46182,7 @@ "cache_read_input_token_cost": 6e-07, "input_cost_per_token": 6e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -46195,6 +46215,7 @@ "cache_read_input_token_cost": 6e-07, "input_cost_per_token": 6e-06, "litellm_provider": "bedrock_converse", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -59505,6 +59526,15 @@ "supports_mid_conversation_system": true } }, + { + "name": "claude-tool-search", + "pattern": "claude-[a-z]+-(?:4[-._](?:[5-9]|[1-9]\\d)(?!\\d)|[5-9](?!\\d)(?:[-._]\\d{1,2}(?!\\d))?)", + "fill_missing_for_providers": ["anthropic", "bedrock", "bedrock_converse", "vertex_ai-anthropic_models"], + "description": "Claude at version 4.5 or higher, in any id shape that contains claude--: minors 4.5 through 4.99, any later major-minor, and bare 5+ majors so a new family like claude-fable-5 matches. Two-digit majors are deliberately not matched so ids like claude-opus-41 (4.1) are not read as major 41. Anthropic's tool search docs list every Claude 4.5 and newer model as supported and Opus 4.1 and earlier as unsupported, so the flag follows the version instead of a per-model list. azure_ai is left out on purpose: Anthropic documents tool search as unavailable on Azure-hosted Foundry deployments, and the azure_ai/ key cannot tell those from Anthropic-hosted ones.", + "model_info": { + "supports_tool_search": true + } + }, { "name": "wandb-reasoning-baseline", "pattern": "^wandb/", @@ -63253,6 +63283,7 @@ "cache_read_input_token_cost": 2.4e-07, "input_cost_per_token": 2.4e-06, "litellm_provider": "bedrock", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -63285,6 +63316,7 @@ "cache_read_input_token_cost": 6e-07, "input_cost_per_token": 6e-06, "litellm_provider": "bedrock", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -63316,6 +63348,7 @@ "cache_read_input_token_cost": 6e-07, "input_cost_per_token": 6e-06, "litellm_provider": "bedrock", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -63456,6 +63489,7 @@ "cache_read_input_token_cost": 2.4e-07, "input_cost_per_token": 2.4e-06, "litellm_provider": "bedrock", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -63488,6 +63522,7 @@ "cache_read_input_token_cost": 6e-07, "input_cost_per_token": 6e-06, "litellm_provider": "bedrock", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, @@ -63519,6 +63554,7 @@ "cache_read_input_token_cost": 6e-07, "input_cost_per_token": 6e-06, "litellm_provider": "bedrock", + "supports_tool_search": true, "max_input_tokens": 1000000, "max_output_tokens": 128000, "max_tokens": 128000, diff --git a/tests/test_litellm/conftest.py b/tests/test_litellm/conftest.py index a4f32df46ae..beca10d5555 100644 --- a/tests/test_litellm/conftest.py +++ b/tests/test_litellm/conftest.py @@ -206,6 +206,21 @@ def local_model_cost_map(monkeypatch): litellm.get_model_info.cache_clear() +@pytest.fixture +def local_beta_headers_config(monkeypatch): + """Pin the bundled ``anthropic_beta_headers_config.json`` so beta header assertions + do not depend on the network-fetched copy or on what earlier tests left cached.""" + from litellm.anthropic_beta_headers_manager import reload_beta_headers_config + + monkeypatch.setenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", "True") + reload_beta_headers_config() + try: + yield + finally: + monkeypatch.delenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", raising=False) + reload_beta_headers_config() + + def _run_coroutine_if_needed(result): if not asyncio.iscoroutine(result): return diff --git a/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py b/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py index 71e6e20b1a4..f7793286c7a 100644 --- a/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py +++ b/tests/test_litellm/litellm_core_utils/test_fallback_generalizations.py @@ -997,5 +997,54 @@ def test_shipped_claude_thinking_rules_backfill_only_anthropic(shipped_cost_map) assert match_fill_missing_generalizations("claude-sonnet-4-6", "anthropic") == { "supports_adaptive_thinking": True, "supports_legacy_thinking": True, + "supports_tool_search": True, } assert match_fill_missing_generalizations("claude-sonnet-4-6", "perplexity") is None + + +@pytest.mark.parametrize( + "model,provider,tool_search", + [ + ("us.anthropic.claude-opus-4-5", "bedrock", True), + ("claude-haiku-4-4", "anthropic", None), + ("claude-haiku-4-6", "anthropic", True), + ("claude-opus-4.5", "anthropic", True), + ("claude-opus-4_5", "anthropic", True), + ("claude-haiku-4-10", "anthropic", True), + ("claude-haiku-5-0", "anthropic", True), + ("claude-sonnet-5-1", "anthropic", True), + ("claude-newfam-6", "anthropic", True), + ("claude-haiku-4-20250514", "anthropic", None), + ], +) +def test_shipped_tool_search_rule_version_boundaries(shipped_cost_map, model, provider, tool_search): + """The claude-tool-search rule flags Claude 4.5 and newer in any family, bare major + or major-minor with a dash, dot or underscore delimiter, and leaves 4.4 and + date-suffixed 4.x ids without an opinion.""" + assert model not in litellm.model_cost + info = litellm.get_model_info(model, custom_llm_provider=provider) + assert info.get("supports_tool_search") is tool_search, model + + +def test_shipped_tool_search_rule_fills_mapped_claude_entries_without_flag(shipped_cost_map): + """A mapped Claude 4.5+ entry with no supports_tool_search key gets it from the rule + on Anthropic direct, Vertex and Bedrock, a mapped pre-4.5 entry stays without one, + and Azure Foundry and reseller copies of the same model are not touched.""" + for key, model, provider in ( + ("claude-opus-4-7", "claude-opus-4-7", "anthropic"), + ("vertex_ai/claude-opus-5", "claude-opus-5", "vertex_ai"), + ): + assert "supports_tool_search" not in litellm.model_cost[key] + assert litellm.get_model_info(model, custom_llm_provider=provider)["supports_tool_search"] is True + + assert "supports_tool_search" not in litellm.model_cost["claude-opus-4-1"] + opus_4_1_info = litellm.get_model_info("claude-opus-4-1", custom_llm_provider="anthropic") + assert opus_4_1_info.get("supports_tool_search") is None + + assert "supports_tool_search" not in litellm.model_cost["azure_ai/claude-opus-5"] + azure_opus_5_info = litellm.get_model_info("claude-opus-5", custom_llm_provider="azure_ai") + assert azure_opus_5_info.get("supports_tool_search") is None + + assert match_fill_missing_generalizations("claude-opus-5", "bedrock")["supports_tool_search"] is True + assert "supports_tool_search" not in match_fill_missing_generalizations("claude-opus-5", "azure_ai") + assert match_fill_missing_generalizations("claude-opus-5", "perplexity") is None diff --git a/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py b/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py index bcba4bf7711..ec0bf6b842a 100644 --- a/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py @@ -814,3 +814,48 @@ async def test_bedrock_invoke_claude_async_completion_inlines_document_url_sourc "type": "document", "source": {"type": "base64", "media_type": "application/pdf", "data": async_only_image_fetch.base64_png}, } in captured["body"]["messages"][0]["content"] + + +@pytest.mark.parametrize( + "model, expected_betas", + [ + pytest.param("us.anthropic.claude-opus-4-8", ["tool-search-tool-2025-10-19"], id="opus_4_8"), + pytest.param("us.anthropic.claude-opus-5", ["tool-search-tool-2025-10-19"], id="opus_5"), + pytest.param("us.anthropic.claude-sonnet-5", ["tool-search-tool-2025-10-19"], id="sonnet_5"), + pytest.param("us.anthropic.claude-haiku-4-5-20251001-v1:0", ["tool-search-tool-2025-10-19"], id="haiku_4_5"), + pytest.param("us.anthropic.claude-opus-4-1-20250805-v1:0", None, id="opus_4_1_unsupported"), + ], +) +def test_bedrock_chat_invoke_tool_search_beta_follows_model_map( + local_model_cost_map, local_beta_headers_config, model, expected_betas +): + """LIT-5851: the chat Invoke path used to add the ``tool-search-tool-2025-10-19`` + beta whenever the id contained ``opus-4``, so Opus 5 and Sonnet 5 lost it, Haiku + 4.5 never had it, and Opus 4.1 got it without support. The gate now follows the + model map's ``supports_tool_search`` flag, shared with the messages path.""" + result = AmazonAnthropicClaudeConfig().transform_request( + model=model, + messages=[{"role": "user", "content": "Add 2 and 3"}], + optional_params={ + "max_tokens": 64, + "tools": [ + {"type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex"}, + { + "type": "function", + "function": { + "name": "add_numbers", + "description": "Add two integers", + "parameters": { + "type": "object", + "properties": {"a": {"type": "integer"}, "b": {"type": "integer"}}, + "required": ["a", "b"], + }, + }, + }, + ], + }, + litellm_params={}, + headers={}, + ) + + assert result.get("anthropic_beta") == expected_betas diff --git a/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py b/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py index 80f917e0578..64a1f255cf8 100644 --- a/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py +++ b/tests/test_litellm/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py @@ -2649,17 +2649,6 @@ def test_bedrock_clear_thinking_leaves_enabled_thinking_on_non_adaptive_model(): assert "output_config" not in request -@pytest.fixture -def local_beta_headers_config(monkeypatch): - from litellm.anthropic_beta_headers_manager import reload_beta_headers_config - - monkeypatch.setenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", "True") - reload_beta_headers_config() - yield - monkeypatch.delenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", raising=False) - reload_beta_headers_config() - - def test_bedrock_messages_preserves_clear_tool_uses_context_management_and_adds_beta( local_beta_headers_config, ): @@ -2825,9 +2814,12 @@ def test_filter_and_transform_beta_headers_passes_context_management_for_bedrock "us.anthropic.claude-haiku-4-5-20251001-v1:0", "us.anthropic.claude-sonnet-4-5-20250929-v1:0", "us.anthropic.claude-opus-4-7", + "us.anthropic.claude-opus-4-8", + "us.anthropic.claude-opus-5", + "us.anthropic.claude-sonnet-5", ], ) -def test_bedrock_messages_tool_search_adds_beta_header(local_beta_headers_config, model): +def test_bedrock_messages_tool_search_adds_beta_header(local_model_cost_map, local_beta_headers_config, model): """ LIT-4522: Bedrock InvokeModel only admits ``tool_search_tool_*`` tool types when the request body carries the ``tool-search-tool-2025-10-19`` beta; @@ -2837,6 +2829,11 @@ def test_bedrock_messages_tool_search_adds_beta_header(local_beta_headers_config Opus 4.7, so the beta was silently dropped for those models and every tool-search request failed. Verified live 2026-08-11: Bedrock returns 200 with ``server_tool_use`` for all three models once the beta is sent. + + LIT-5851: the same allowlist then missed Opus 4.8, Opus 5 and Sonnet 5, so + the gate now reads the model map's ``supports_tool_search`` flag (explicit + on the Bedrock entries, and the ``claude-tool-search`` rule for Claude 4.5 + and newer) instead of a per-model name list. """ from litellm.types.router import GenericLiteLLMParams @@ -2870,10 +2867,10 @@ def test_bedrock_messages_tool_search_adds_beta_header(local_beta_headers_config def test_bedrock_messages_tool_search_model_map_flag_is_authoritative(local_model_cost_map, monkeypatch): - """``supports_tool_search`` lives in the model map; the name patterns in - ``_supports_tool_search_on_bedrock`` are only a fallback for ids the map - cannot resolve. Flipping the mapped entry's flag to ``False`` must win even - though the model name still matches the ``haiku-4-5`` pattern.""" + """``supports_tool_search`` lives in the model map; the ``claude-tool-search`` + rule only fills entries that carry no opinion. Flipping the mapped entry's + flag to ``False`` must win even though the id is a Claude 4.5 the rule + would flag.""" import litellm from litellm.llms.anthropic.common_utils import AnthropicModelInfo @@ -2892,19 +2889,43 @@ def test_bedrock_messages_tool_search_model_map_flag_is_authoritative(local_mode @pytest.mark.parametrize( "model, expected", [ - pytest.param("us.anthropic.claude-opus-4-6-v99:9", True, id="unmapped_id_falls_back_to_patterns"), - pytest.param("anthropic.claude-3-5-sonnet-20240620-v1:0", False, id="mapped_entry_without_flag_no_pattern"), + pytest.param("us.anthropic.claude-opus-4-6-v99:9", True, id="unmapped_4_6_variant"), + pytest.param("us.anthropic.claude-haiku-5-2", True, id="unmapped_future_minor"), + pytest.param( + "arn:aws:bedrock:us-east-1:123456789012:inference-profile/us.anthropic.claude-opus-5", + True, + id="inference_profile_arn", + ), + pytest.param("anthropic.claude-3-5-sonnet-20240620-v1:0", False, id="mapped_claude_3_5_without_flag"), + pytest.param("us.anthropic.claude-opus-4-1-20250805-v1:0", False, id="mapped_opus_4_1_without_flag"), + pytest.param("us.anthropic.claude-sonnet-4-20250514-v1:0", False, id="mapped_dated_sonnet_4_without_flag"), ], ) -def test_bedrock_messages_tool_search_pattern_fallback(local_model_cost_map, model, expected): - """Ids the model map cannot resolve (or resolves without a - ``supports_tool_search`` opinion) fall through to the name patterns, so - ARNs and unlisted regional variants of supported families keep working.""" +def test_bedrock_messages_tool_search_follows_claude_tool_search_rule(local_model_cost_map, model, expected): + """Ids the model map cannot resolve, or resolves without a ``supports_tool_search`` + opinion, take the ``claude-tool-search`` fallback rule: Claude 4.5 and newer get + the beta, ARNs and unlisted regional variants included, and older Claudes do not.""" cfg = AmazonAnthropicClaudeMessagesConfig() assert cfg._supports_tool_search_on_bedrock(model) is expected +def test_bedrock_messages_tool_search_rule_fills_mapped_entry_without_flag(local_model_cost_map, monkeypatch): + """LIT-5851: a Bedrock entry that is in the map but carries no ``supports_tool_search`` + key, the state Opus 4.8, Opus 5 and Sonnet 5 shipped in, is filled by the + ``claude-tool-search`` rule instead of resolving to ``None`` and losing the beta.""" + import litellm + + model = "us.anthropic.claude-opus-5" + cfg = AmazonAnthropicClaudeMessagesConfig() + + monkeypatch.delitem(litellm.model_cost[model], "supports_tool_search") + litellm.get_model_info.cache_clear() + + assert litellm.get_model_info(model, custom_llm_provider="bedrock")["supports_tool_search"] is True + assert cfg._supports_tool_search_on_bedrock(model) is True + + def test_bedrock_messages_thinking_shape_follows_exact_bedrock_entry_flag( local_model_cost_map, monkeypatch ):