mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-24 05:45:11 +00:00
fix(anthropic): drop safeguards on routes without the classifier only under drop_params
A route whose upstream does not answer safeguards (a JSON provider, or a map that marks the dangerous-tool-use beta unsupported) now forwards the field and the beta as the merge base did unless drop_params is set or the proxy runs its own classifier, matching how litellm treats any unsupported param. Databricks keeps dropping both, since its merge base bridge never forwarded the field.
This commit is contained in:
parent
6961b70cb3
commit
84efce3412
7 changed files with 93 additions and 34 deletions
|
|
@ -40,8 +40,7 @@ from ..responses_adapters.handler import LiteLLMMessagesToResponsesAPIHandler
|
|||
from ..safeguards import (
|
||||
SafeguardsEvaluator,
|
||||
native_route_classifies,
|
||||
read_classifier_model_setting,
|
||||
requested_dangerous_tool_use,
|
||||
proxy_serves_safeguards,
|
||||
with_safeguard_results,
|
||||
)
|
||||
from .interceptors import get_messages_interceptors
|
||||
|
|
@ -166,7 +165,7 @@ def _native_messages_config(
|
|||
|
||||
|
||||
async def _proxy_classifies(model: str, safeguards: object, native_config: BaseAnthropicMessagesConfig | None) -> bool:
|
||||
if requested_dangerous_tool_use(safeguards) is None or read_classifier_model_setting() is None:
|
||||
if not proxy_serves_safeguards(safeguards):
|
||||
return False
|
||||
if native_config is None:
|
||||
return True
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@ from typing import ClassVar, Final
|
|||
|
||||
import httpx
|
||||
|
||||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.anthropic_beta_headers_manager import get_unsupported_headers, is_beta_header_supported_by_bundled_config
|
||||
from litellm.exceptions import AuthenticationError
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
||||
from litellm.llms.anthropic.pass_through.safeguards import proxy_serves_safeguards
|
||||
from litellm.llms.base_llm.anthropic_messages.transformation import (
|
||||
BaseAnthropicMessagesConfig,
|
||||
)
|
||||
|
|
@ -112,17 +114,25 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
|
||||
def supports_safeguards(self) -> bool:
|
||||
"""
|
||||
The ``safeguards`` body field and the dangerous-tool-use beta travel together: a
|
||||
platform whose beta header map column marks the beta unsupported gets neither, so the
|
||||
turn stays a 200 without a verdict instead of a 400 on an unknown field. A platform the
|
||||
map does not list keeps the body field, and the beta goes with it only where that
|
||||
platform's config forwards betas unfiltered.
|
||||
Whether the upstream platform answers the ``safeguards`` body field itself. A platform
|
||||
whose beta header map column marks the dangerous-tool-use beta unsupported does not, and
|
||||
a platform the map does not list is trusted to unless its config says otherwise.
|
||||
"""
|
||||
return not self.should_filter_anthropic_beta_headers() or (
|
||||
ANTHROPIC_BETA_HEADER_VALUES.DANGEROUS_TOOL_USE_2026_09_03.value
|
||||
not in get_unsupported_headers(self._resolved_provider)
|
||||
)
|
||||
|
||||
def drops_safeguards(self, safeguards: object, drop_params: bool) -> bool:
|
||||
"""
|
||||
``safeguards`` and the dangerous-tool-use beta leave together for a platform that does not
|
||||
answer them, when the proxy classifies in its place or drop_params is set. Otherwise both
|
||||
are forwarded as sent and the platform answers its own 400, like any unsupported param.
|
||||
"""
|
||||
return not self.supports_safeguards() and (
|
||||
drop_params or litellm.drop_params is True or proxy_serves_safeguards(safeguards)
|
||||
)
|
||||
|
||||
def _remove_scope_from_cache_control(self, anthropic_messages_request: dict) -> None:
|
||||
"""
|
||||
Remove `scope` field from cache_control blocks.
|
||||
|
|
@ -610,7 +620,10 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
status_code=400,
|
||||
)
|
||||
|
||||
if "safeguards" in anthropic_messages_optional_request_params and not self.supports_safeguards():
|
||||
if "safeguards" in anthropic_messages_optional_request_params and self.drops_safeguards(
|
||||
anthropic_messages_optional_request_params["safeguards"], # pyright: ignore[reportUnknownArgumentType] # the request params arrive as a bare dict
|
||||
litellm_params.drop_params is True,
|
||||
):
|
||||
anthropic_messages_optional_request_params.pop("safeguards") # pyright: ignore[reportUnknownMemberType] # the request params arrive as a bare dict
|
||||
|
||||
self._translate_reasoning_effort_to_anthropic(
|
||||
|
|
@ -731,6 +744,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
custom_llm_provider: str = "anthropic",
|
||||
messages: Sequence[object] = (),
|
||||
beta_map_provider: str | None = None,
|
||||
drop_params: bool = False,
|
||||
) -> dict:
|
||||
"""
|
||||
Auto-inject anthropic-beta headers based on features used.
|
||||
|
|
@ -748,6 +762,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
custom_llm_provider: Provider name for looking up correct tool search header
|
||||
messages: Request messages, scanned for per-message output_config
|
||||
beta_map_provider: Provider whose beta map column gates safeguards, defaults to custom_llm_provider
|
||||
drop_params: Whether the deployment or request set drop_params
|
||||
"""
|
||||
beta_values: Final[set[str]] = set()
|
||||
|
||||
|
|
@ -829,7 +844,11 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
dangerous_tool_use: Final = ANTHROPIC_BETA_HEADER_VALUES.DANGEROUS_TOOL_USE_2026_09_03.value
|
||||
forwarded_beta_values: Final = ( # pyright: ignore[reportUnknownVariableType] # derived from the bare beta set
|
||||
all_beta_values - {dangerous_tool_use}
|
||||
if dangerous_tool_use in all_beta_values and not self.supports_safeguards()
|
||||
if dangerous_tool_use in all_beta_values
|
||||
and self.drops_safeguards(
|
||||
optional_params.get("safeguards"), # pyright: ignore[reportUnknownArgumentType] # the optional params arrive as a bare dict
|
||||
drop_params,
|
||||
)
|
||||
else all_beta_values
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -273,12 +273,12 @@ def read_classifier_model_setting() -> str | None:
|
|||
return configured if isinstance(configured, str) and configured else None
|
||||
|
||||
|
||||
def proxy_serves_safeguards(safeguards: object) -> bool:
|
||||
return requested_dangerous_tool_use(safeguards) is not None and read_classifier_model_setting() is not None
|
||||
|
||||
|
||||
def native_route_classifies(model: str, safeguards: object) -> bool:
|
||||
return (
|
||||
"claude" not in model.lower()
|
||||
and requested_dangerous_tool_use(safeguards) is not None
|
||||
and read_classifier_model_setting() is not None
|
||||
)
|
||||
return "claude" not in model.lower() and proxy_serves_safeguards(safeguards)
|
||||
|
||||
|
||||
def build_safeguards_evaluator(
|
||||
|
|
|
|||
|
|
@ -93,6 +93,9 @@ class DatabricksAnthropicMessagesConfig(DatabricksBase, AnthropicMessagesConfig)
|
|||
def handles_web_search_natively(self) -> bool:
|
||||
return False
|
||||
|
||||
def drops_safeguards(self, safeguards: object, drop_params: bool) -> bool:
|
||||
return not self.supports_safeguards()
|
||||
|
||||
def serves_api_base(self, model: str, api_base: str | None) -> bool:
|
||||
resolved_api_base: Final = self.resolve_api_base(api_base)
|
||||
return resolved_api_base is None or self.recognizes_workspace_route(resolved_api_base, model)
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ class OpenAILikeAnthropicMessagesConfig(AnthropicMessagesConfig):
|
|||
optional_params=optional_params,
|
||||
messages=messages,
|
||||
beta_map_provider=LlmProviders.OPENAI_LIKE.value,
|
||||
drop_params=litellm_params.get("drop_params") is True, # pyright: ignore[reportUnknownMemberType] # the litellm params arrive as a bare dict
|
||||
)
|
||||
return merged, api_base
|
||||
|
||||
|
|
|
|||
|
|
@ -533,12 +533,23 @@ _PROVIDERS: Final = MappingProxyType(
|
|||
client_beta=_CLIENT_BETAS_WITH_DANGEROUS,
|
||||
auth=("authorization", "Bearer synthetic-sail-key"),
|
||||
expected_beta=None,
|
||||
),
|
||||
"D18_sail_with_claude_code_header_and_drop_params": _ProviderCase(
|
||||
parameters={"model": "sail/sail-claude", "api_key": "synthetic-sail-key", "drop_params": True},
|
||||
client_beta=_CLIENT_BETAS_WITH_DANGEROUS,
|
||||
auth=("authorization", "Bearer synthetic-sail-key"),
|
||||
expected_beta=None,
|
||||
forwards_safeguards=False,
|
||||
),
|
||||
"D19_edenai": _ProviderCase(
|
||||
parameters={"model": "edenai/claude-sonnet-4-5", "api_key": "synthetic-edenai-key"},
|
||||
auth=("authorization", "Bearer synthetic-edenai-key"),
|
||||
expected_beta=None,
|
||||
),
|
||||
"D19_edenai_with_drop_params": _ProviderCase(
|
||||
parameters={"model": "edenai/claude-sonnet-4-5", "api_key": "synthetic-edenai-key", "drop_params": True},
|
||||
auth=("authorization", "Bearer synthetic-edenai-key"),
|
||||
expected_beta=None,
|
||||
forwards_safeguards=False,
|
||||
),
|
||||
"D21_bedrock_claude_platform": _ProviderCase(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -160,12 +162,12 @@ def _configs_that_forward_safeguards_as_sent() -> tuple[AnthropicMessagesConfig,
|
|||
)
|
||||
|
||||
|
||||
def _safeguards_request(config: AnthropicMessagesConfig) -> dict:
|
||||
def _safeguards_request(config: AnthropicMessagesConfig, litellm_params: GenericLiteLLMParams | None = None) -> dict:
|
||||
return config.transform_anthropic_messages_request(
|
||||
model="claude-sonnet-4-6",
|
||||
messages=[{"role": "user", "content": "Use the Bash tool to run: echo hello"}],
|
||||
anthropic_messages_optional_request_params={"max_tokens": 64, "safeguards": list(AUTO_MODE_SAFEGUARDS)},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
litellm_params=litellm_params or GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
|
|
@ -206,27 +208,50 @@ def test_unjudged_routes_forward_safeguards_and_the_beta_as_sent(
|
|||
assert _derived_beta_header(config)["anthropic-beta"] == DANGEROUS_TOOL_USE_BETA
|
||||
|
||||
|
||||
_CLASSIFIER_SETTINGS: Final = {"safeguards_classifier_model": "classifier"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config", _no_classifier_configs(), ids=type)
|
||||
def test_routes_without_the_classifier_drop_safeguards_together_with_the_beta(
|
||||
config: AnthropicMessagesConfig, local_beta_headers_config: None
|
||||
@pytest.mark.parametrize(
|
||||
("litellm_params", "global_drop_params", "general_settings", "forwarded"),
|
||||
(
|
||||
pytest.param(GenericLiteLLMParams(), False, {}, AUTO_MODE_SAFEGUARDS, id="as_sent"),
|
||||
pytest.param(GenericLiteLLMParams(drop_params=True), False, {}, None, id="deployment_drop_params"),
|
||||
pytest.param(GenericLiteLLMParams(), True, {}, None, id="global_drop_params"),
|
||||
pytest.param(GenericLiteLLMParams(), False, _CLASSIFIER_SETTINGS, None, id="proxy_classifier"),
|
||||
),
|
||||
)
|
||||
def test_a_route_without_the_classifier_drops_safeguards_only_under_drop_params_or_the_proxy_classifier(
|
||||
config: AnthropicMessagesConfig,
|
||||
litellm_params: GenericLiteLLMParams,
|
||||
global_drop_params: bool,
|
||||
general_settings: Mapping[str, str],
|
||||
forwarded: list[dict] | None,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
local_beta_headers_config: None,
|
||||
) -> None:
|
||||
"""A platform without the classifier answers 400 "safeguards: Extra inputs are not permitted"
|
||||
to the body field whatever the header says, so the pair is dropped together and the turn
|
||||
stays a 200 without a verdict, which Claude Code's gateway contract treats as "feature off"."""
|
||||
assert "safeguards" not in _safeguards_request(config)
|
||||
assert "anthropic-beta" not in _beta_header(config, DANGEROUS_TOOL_USE_BETA)
|
||||
assert "anthropic-beta" not in _derived_beta_header(config)
|
||||
assert _beta_header(config, f"context-1m-2025-08-07,{DANGEROUS_TOOL_USE_BETA}") == {
|
||||
"anthropic-beta": "context-1m-2025-08-07"
|
||||
}
|
||||
"""A platform without the classifier answers 400 to `safeguards`, which is how litellm treats any
|
||||
unsupported param: dropped under drop_params, forwarded as sent without it. The proxy's own
|
||||
classifier answers in the platform's place, so the field is dropped then too."""
|
||||
monkeypatch.setattr(litellm, "drop_params", global_drop_params)
|
||||
with patch("litellm.proxy.proxy_server.general_settings", general_settings):
|
||||
request: Final = _safeguards_request(config, litellm_params)
|
||||
assert request.get("safeguards") == forwarded
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_classifier_route_sends_neither_safeguards_nor_the_beta_on_the_wire(
|
||||
local_beta_headers_config: None,
|
||||
@pytest.mark.parametrize(
|
||||
("drop_params", "forwarded", "forwarded_beta"),
|
||||
(
|
||||
pytest.param(False, AUTO_MODE_SAFEGUARDS, f"context-1m-2025-08-07,{DANGEROUS_TOOL_USE_BETA}", id="as_sent"),
|
||||
pytest.param(True, None, "context-1m-2025-08-07", id="drop_params"),
|
||||
),
|
||||
)
|
||||
async def test_a_json_provider_gets_safeguards_and_the_beta_together_unless_drop_params_drops_both(
|
||||
drop_params: bool, forwarded: list[dict] | None, forwarded_beta: str, local_beta_headers_config: None
|
||||
) -> None:
|
||||
"""JSON-configured OpenAI-compatible providers forward every beta header untouched, so the
|
||||
pair has to be dropped by the request transform itself, not by the beta header filter."""
|
||||
"""JSON-configured OpenAI-compatible providers forward every beta header untouched, so under
|
||||
drop_params the request transform itself drops the beta along with the field."""
|
||||
import json
|
||||
|
||||
import httpx
|
||||
|
|
@ -263,6 +288,7 @@ async def test_no_classifier_route_sends_neither_safeguards_nor_the_beta_on_the_
|
|||
api_base="https://sail.example.test/v1",
|
||||
safeguards=list(AUTO_MODE_SAFEGUARDS),
|
||||
extra_headers={"anthropic-beta": f"context-1m-2025-08-07,{DANGEROUS_TOOL_USE_BETA}"},
|
||||
drop_params=drop_params,
|
||||
client=upstream,
|
||||
)
|
||||
finally:
|
||||
|
|
@ -270,7 +296,7 @@ async def test_no_classifier_route_sends_neither_safeguards_nor_the_beta_on_the_
|
|||
|
||||
assert len(received) == 1
|
||||
sent: Final = received[0]
|
||||
assert "safeguards" not in json.loads(sent.content)
|
||||
assert sent.headers["anthropic-beta"] == "context-1m-2025-08-07"
|
||||
assert json.loads(sent.content).get("safeguards") == forwarded
|
||||
assert sent.headers["anthropic-beta"] == forwarded_beta
|
||||
assert "safeguard_results" not in response
|
||||
assert response["content"][0]["text"] == "hello"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue