fix(anthropic): strip safeguards on the adapter path and type it on streaming chunks

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yassin 2026-09-20 17:37:56 +00:00
parent 3ebd5add32
commit b59028d525
4 changed files with 16 additions and 7 deletions

View file

@ -35,7 +35,7 @@ if TYPE_CHECKING:
from litellm.router import Router
# Anthropic-only keys already mapped by the translator; strip on extra_kwargs re-merge.
ANTHROPIC_ONLY_REQUEST_KEYS: Final[frozenset[str]] = frozenset({"output_config"})
ANTHROPIC_ONLY_REQUEST_KEYS: Final[frozenset[str]] = frozenset({"output_config", "safeguards"})
_AnthropicMessages: TypeAlias = "list[dict[str, object]]"
_AnthropicSystem: TypeAlias = "str | list[dict[str, object]] | None"

View file

@ -531,6 +531,7 @@ class AnthropicStopDetails(TypedDict, total=False):
class MessageDelta(TypedDict, total=False):
stop_reason: str | None
stop_details: ReadOnly[AnthropicStopDetails]
safeguard_results: ReadOnly[dict[str, object]]
class ServerToolUsage(TypedDict, total=False):
@ -601,6 +602,7 @@ class MessageChunk(TypedDict, total=False):
stop_reason: str | None
stop_sequence: str | None
usage: UsageDelta
safeguard_results: ReadOnly[dict[str, object]]
class MessageStartBlock(TypedDict):

View file

@ -110,6 +110,19 @@ class TestOutputConfigStrippedFromCompletionKwargs:
"reject it with 400 'Extra inputs are not permitted'"
)
def test_safeguards_is_stripped_for_non_anthropic_target(self):
extra_kwargs = {
"custom_llm_provider": "azure",
"safeguards": {"auto_mode": {"enabled": True, "version": "2026-09-01"}},
}
result = _call_prepare(extra_kwargs=extra_kwargs)
completion_kwargs = result[0] if isinstance(result, tuple) else result
assert "safeguards" not in completion_kwargs, (
"safeguards is an Anthropic-only field; OpenAI-format backends reject it with 400"
)
def test_output_config_format_translated_to_response_format(self):
"""When ``output_config`` carries structured-output ``format``, the
translator now maps it to OpenAI's ``response_format`` so non-Anthropic

View file

@ -1442,9 +1442,6 @@ async def test_anthropic_messages_leaves_non_provider_failures_unmapped():
@pytest.mark.asyncio
async def test_anthropic_messages_forwards_safeguards_and_unknown_beta_to_anthropic():
"""Regression test for LIT-8232. Claude Code auto mode sends a `safeguards` body
field paired with a beta value the gateway has never seen. Both must reach
api.anthropic.com unchanged or the session falls back to billed classifier calls."""
from litellm.llms.anthropic.experimental_pass_through.messages import handler
safeguards = {"auto_mode": {"enabled": True, "version": "2026-09-01"}}
@ -1491,9 +1488,6 @@ async def test_anthropic_messages_forwards_safeguards_and_unknown_beta_to_anthro
@pytest.mark.asyncio
async def test_anthropic_messages_streaming_forwards_safeguards_and_keeps_safeguard_results():
"""Streaming sibling of the LIT-8232 regression: the request must still carry
`safeguards` and the `safeguard_results` Anthropic emits on `message_start` and
`message_delta` must reach the client byte for byte."""
from litellm.llms.anthropic.experimental_pass_through.messages import handler
safeguards = {"auto_mode": {"enabled": True, "version": "2026-09-01"}}