mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-24 00:52:24 +00:00
fix(anthropic): add the dangerous-tool-use beta when safeguards arrive without it on Bedrock Invoke and Vertex
This commit is contained in:
parent
deca6aea79
commit
21b5e2d8a1
6 changed files with 111 additions and 15 deletions
|
|
@ -530,6 +530,9 @@ class AmazonAnthropicClaudeMessagesConfig(
|
|||
if anthropic_model_info.is_eager_input_streaming_used(tools):
|
||||
beta_set.add(ANTHROPIC_FINE_GRAINED_TOOL_STREAMING_BETA_HEADER)
|
||||
|
||||
if anthropic_messages_optional_request_params.get("safeguards") is not None:
|
||||
beta_set.add(ANTHROPIC_BETA_HEADER_VALUES.DANGEROUS_TOOL_USE_2026_09_03.value)
|
||||
|
||||
self._filter_context_management_for_bedrock_invoke(
|
||||
anthropic_messages_request=anthropic_messages_request,
|
||||
beta_set=beta_set,
|
||||
|
|
|
|||
|
|
@ -108,6 +108,9 @@ class VertexAIPartnerModelsAnthropicMessagesConfig(AnthropicMessagesConfig, Vert
|
|||
if anthropic_model_info.is_tool_search_used(tools):
|
||||
beta_values.add(get_tool_search_beta_header("vertex_ai"))
|
||||
|
||||
if optional_params.get("safeguards") is not None:
|
||||
beta_values.add(ANTHROPIC_BETA_HEADER_VALUES.DANGEROUS_TOOL_USE_2026_09_03.value)
|
||||
|
||||
if beta_values:
|
||||
headers["anthropic-beta"] = ",".join(beta_values)
|
||||
|
||||
|
|
|
|||
|
|
@ -751,6 +751,7 @@ class ANTHROPIC_BETA_HEADER_VALUES(str, Enum):
|
|||
FAST_MODE_2026_02_01 = "fast-mode-2026-02-01"
|
||||
ADVISOR_TOOL_2026_03_01 = "advisor-tool-2026-03-01"
|
||||
PER_TURN_CONTROL_2026_07_01 = "per-turn-control-2026-07-01"
|
||||
DANGEROUS_TOOL_USE_2026_09_03 = "dangerous-tool-use-2026-09-03"
|
||||
|
||||
|
||||
# Tool search beta header constant (for Anthropic direct API and Microsoft Foundry)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import asyncio
|
|||
import json
|
||||
import os
|
||||
import uuid
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, Final, List
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
|
@ -1579,11 +1579,19 @@ def _upstream_answering_with(safeguard_results: list[dict[str, object]], capture
|
|||
return upstream
|
||||
|
||||
|
||||
_CLIENT_BETA_HEADERS: Final = (
|
||||
pytest.param({"anthropic-beta": "dangerous-tool-use-2026-09-03,interleaved-thinking-2025-05-14"}, id="client_sends_beta"),
|
||||
pytest.param({"anthropic-beta": "interleaved-thinking-2025-05-14"}, id="client_omits_beta"),
|
||||
pytest.param({}, id="client_sends_no_beta_header"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("client_headers", _CLIENT_BETA_HEADERS)
|
||||
async def test_anthropic_messages_forwards_safeguards_and_dangerous_tool_use_beta_to_bedrock_invoke(
|
||||
local_beta_headers_config,
|
||||
local_beta_headers_config, client_headers
|
||||
):
|
||||
"""Bedrock Invoke takes betas in the body's `anthropic_beta` and 400s on `safeguards` without the beta."""
|
||||
"""Bedrock Invoke takes betas in the body's `anthropic_beta` and 400s on `safeguards` without the beta, so the beta rides along with the field."""
|
||||
from litellm.llms.anthropic.experimental_pass_through.messages import handler
|
||||
|
||||
safeguards, safeguard_results = _claude_code_auto_mode_request()
|
||||
|
|
@ -1599,7 +1607,7 @@ async def test_anthropic_messages_forwards_safeguards_and_dangerous_tool_use_bet
|
|||
aws_region_name="us-east-1",
|
||||
client=_upstream_answering_with(safeguard_results, captured),
|
||||
safeguards=safeguards,
|
||||
extra_headers={"anthropic-beta": "dangerous-tool-use-2026-09-03,interleaved-thinking-2025-05-14"},
|
||||
extra_headers=client_headers,
|
||||
)
|
||||
|
||||
assert captured["body"]["safeguards"] == safeguards
|
||||
|
|
@ -1608,10 +1616,11 @@ async def test_anthropic_messages_forwards_safeguards_and_dangerous_tool_use_bet
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("client_headers", _CLIENT_BETA_HEADERS)
|
||||
async def test_anthropic_messages_forwards_safeguards_and_dangerous_tool_use_beta_to_vertex(
|
||||
local_beta_headers_config,
|
||||
local_beta_headers_config, client_headers
|
||||
):
|
||||
"""Vertex rawPredict takes the beta as the `anthropic-beta` header and 400s on `safeguards` without it."""
|
||||
"""Vertex rawPredict takes the beta as the `anthropic-beta` header and 400s on `safeguards` without it, so the beta rides along with the field."""
|
||||
from litellm.llms.anthropic.experimental_pass_through.messages import handler
|
||||
from litellm.llms.vertex_ai.vertex_llm_base import VertexBase
|
||||
|
||||
|
|
@ -1629,13 +1638,10 @@ async def test_anthropic_messages_forwards_safeguards_and_dangerous_tool_use_bet
|
|||
vertex_credentials="{}",
|
||||
client=_upstream_answering_with(safeguard_results, captured),
|
||||
safeguards=safeguards,
|
||||
extra_headers={"anthropic-beta": "dangerous-tool-use-2026-09-03,interleaved-thinking-2025-05-14"},
|
||||
extra_headers=client_headers,
|
||||
)
|
||||
|
||||
assert captured["body"]["safeguards"] == safeguards
|
||||
assert "anthropic_beta" not in captured["body"]
|
||||
assert set(captured["anthropic-beta"].split(",")) == {
|
||||
"dangerous-tool-use-2026-09-03",
|
||||
"interleaved-thinking-2025-05-14",
|
||||
}
|
||||
assert captured["anthropic-beta"].split(",").count("dangerous-tool-use-2026-09-03") == 1
|
||||
assert response["safeguard_results"] == safeguard_results
|
||||
|
|
|
|||
|
|
@ -1651,12 +1651,19 @@ def test_bedrock_messages_allowlist_filters_anthropic_only_fields():
|
|||
assert set(result).issubset(cfg.BEDROCK_INVOKE_ALLOWED_TOP_LEVEL_FIELDS)
|
||||
|
||||
|
||||
def test_bedrock_messages_forwards_safeguards_with_dangerous_tool_use_beta(local_beta_headers_config):
|
||||
@pytest.mark.parametrize(
|
||||
"client_beta_header",
|
||||
["dangerous-tool-use-2026-09-03,interleaved-thinking-2025-05-14", "interleaved-thinking-2025-05-14"],
|
||||
ids=["client_sends_beta", "client_omits_beta"],
|
||||
)
|
||||
def test_bedrock_messages_forwards_safeguards_with_dangerous_tool_use_beta(local_beta_headers_config, client_beta_header):
|
||||
"""
|
||||
Claude Code's server-side auto-mode classifier sends `safeguards` alongside the
|
||||
dangerous-tool-use-2026-09-03 beta. Bedrock Invoke accepts the pair, answers
|
||||
"safeguards: Extra inputs are not permitted" for the field alone, and returns
|
||||
`safeguard_results: []` for the beta alone, so both must reach it unchanged.
|
||||
`safeguard_results: []` for the beta alone, so the field reaches it unchanged
|
||||
and the beta rides along whether or not the client sent it, as every other
|
||||
body-driven beta does here.
|
||||
"""
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
|
|
@ -1668,11 +1675,28 @@ def test_bedrock_messages_forwards_safeguards_with_dangerous_tool_use_beta(local
|
|||
messages=[{"role": "user", "content": [{"type": "text", "text": "Hello"}]}],
|
||||
anthropic_messages_optional_request_params={"max_tokens": 64, "safeguards": safeguards},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={"anthropic-beta": "dangerous-tool-use-2026-09-03,interleaved-thinking-2025-05-14"},
|
||||
headers={"anthropic-beta": client_beta_header},
|
||||
)
|
||||
|
||||
assert result["safeguards"] == safeguards
|
||||
assert "dangerous-tool-use-2026-09-03" in result["anthropic_beta"]
|
||||
assert result["anthropic_beta"].count("dangerous-tool-use-2026-09-03") == 1
|
||||
|
||||
|
||||
def test_bedrock_messages_does_not_add_dangerous_tool_use_beta_without_safeguards(local_beta_headers_config):
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
cfg = AmazonAnthropicClaudeMessagesConfig()
|
||||
|
||||
result = cfg.transform_anthropic_messages_request(
|
||||
model="us.anthropic.claude-sonnet-5",
|
||||
messages=[{"role": "user", "content": [{"type": "text", "text": "Hello"}]}],
|
||||
anthropic_messages_optional_request_params={"max_tokens": 64},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert "safeguards" not in result
|
||||
assert "dangerous-tool-use-2026-09-03" not in result.get("anthropic_beta", [])
|
||||
|
||||
|
||||
def test_bedrock_messages_stream_decoder_keeps_safeguard_results():
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import json
|
|||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.llms.vertex_ai.vertex_ai_partner_models.anthropic.experimental_pass_through.transformation import (
|
||||
VertexAIPartnerModelsAnthropicMessagesConfig,
|
||||
)
|
||||
|
|
@ -67,6 +69,63 @@ def test_web_search_header_added_for_messages_endpoint():
|
|||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"client_headers",
|
||||
[{"anthropic-beta": "dangerous-tool-use-2026-09-03"}, {}],
|
||||
ids=["client_sends_beta", "client_omits_beta"],
|
||||
)
|
||||
def test_safeguards_add_dangerous_tool_use_beta_header(client_headers):
|
||||
"""Vertex rejects `safeguards` without the dangerous-tool-use beta, so the beta rides along with the field the way the web search and context management betas do."""
|
||||
config = VertexAIPartnerModelsAnthropicMessagesConfig()
|
||||
litellm_params = {
|
||||
"vertex_ai_project": "test-project",
|
||||
"vertex_ai_location": "global",
|
||||
"vertex_credentials": "{}",
|
||||
}
|
||||
optional_params = {
|
||||
"safeguards": [{"type": "dangerous_tool_use", "classifier_context": {"v": 1, "permission_mode": "auto"}}]
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(config, "_ensure_access_token", return_value=("token", "test-project")),
|
||||
patch.object(config, "get_complete_vertex_url", return_value="https://mock-url"),
|
||||
):
|
||||
updated_headers, _ = config.validate_anthropic_messages_environment(
|
||||
headers=client_headers,
|
||||
model="claude-sonnet-5",
|
||||
messages=[],
|
||||
optional_params=optional_params,
|
||||
litellm_params=litellm_params,
|
||||
api_base=None,
|
||||
)
|
||||
|
||||
assert updated_headers["anthropic-beta"].split(",").count("dangerous-tool-use-2026-09-03") == 1
|
||||
|
||||
|
||||
def test_no_safeguards_leaves_dangerous_tool_use_beta_header_out():
|
||||
config = VertexAIPartnerModelsAnthropicMessagesConfig()
|
||||
litellm_params = {
|
||||
"vertex_ai_project": "test-project",
|
||||
"vertex_ai_location": "global",
|
||||
"vertex_credentials": "{}",
|
||||
}
|
||||
|
||||
with (
|
||||
patch.object(config, "_ensure_access_token", return_value=("token", "test-project")),
|
||||
patch.object(config, "get_complete_vertex_url", return_value="https://mock-url"),
|
||||
):
|
||||
updated_headers, _ = config.validate_anthropic_messages_environment(
|
||||
headers={},
|
||||
model="claude-sonnet-5",
|
||||
messages=[],
|
||||
optional_params={"max_tokens": 64},
|
||||
litellm_params=litellm_params,
|
||||
api_base=None,
|
||||
)
|
||||
|
||||
assert "dangerous-tool-use-2026-09-03" not in updated_headers.get("anthropic-beta", "")
|
||||
|
||||
|
||||
def test_web_search_header_not_added_without_tool():
|
||||
"""Test that beta header is NOT added when web search tool is not present"""
|
||||
config = VertexAIPartnerModelsAnthropicMessagesConfig()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue