fix(anthropic): type safeguards and safeguard_results as the arrays Anthropic sends

Driving a real Claude Code 2.1.278 through the proxy, and a direct call to
api.anthropic.com, both show these two fields are JSON arrays on the wire rather
than objects. The request carries safeguards as
[{"type": "dangerous_tool_use", "classifier_context": {...}}] under beta
dangerous-tool-use-2026-09-03, and the 200 comes back with safeguard_results as
[{"type": "dangerous_tool_use", "status": {"type": "available", "tool_uses": {...}}}].

No runtime change: the request filter matches on TypedDict keys and never inspects
the value. The test fixtures move to the captured shapes so the regression tests
pin what the client and the provider actually exchange.
This commit is contained in:
Yassin Kortam 2026-09-21 09:55:09 -05:00
parent b59028d525
commit 1ac4d7ae04
4 changed files with 15 additions and 11 deletions

View file

@ -411,7 +411,7 @@ class AnthropicMessagesRequestOptionalParams(TypedDict, total=False):
output_config: AnthropicOutputConfig | None # Configuration for Claude's output behavior
cache_control: dict[str, Any] | None # Automatic prompt caching
reasoning_effort: str | None
safeguards: ReadOnly[dict[str, object] | None]
safeguards: ReadOnly[list[dict[str, object]] | None]
class AnthropicMessagesRequest(AnthropicMessagesRequestOptionalParams, total=False):
@ -531,7 +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]]
safeguard_results: ReadOnly[list[dict[str, object]]]
class ServerToolUsage(TypedDict, total=False):
@ -602,7 +602,7 @@ class MessageChunk(TypedDict, total=False):
stop_reason: str | None
stop_sequence: str | None
usage: UsageDelta
safeguard_results: ReadOnly[dict[str, object]]
safeguard_results: ReadOnly[list[dict[str, object]]]
class MessageStartBlock(TypedDict):

View file

@ -97,4 +97,4 @@ class AnthropicMessagesResponse(TypedDict, total=False):
type: Literal["message"] | None
usage: AnthropicUsage | None
context_management: NotRequired[ContextManagementResponse]
safeguard_results: NotRequired[ReadOnly[dict[str, object]]]
safeguard_results: NotRequired[ReadOnly[list[dict[str, object]]]]

View file

@ -113,7 +113,7 @@ class TestOutputConfigStrippedFromCompletionKwargs:
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"}},
"safeguards": [{"type": "dangerous_tool_use", "classifier_context": {"v": 1}}],
}
result = _call_prepare(extra_kwargs=extra_kwargs)

View file

@ -1442,10 +1442,12 @@ 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():
"""Shapes are what Claude Code 2.1.278 sends and api.anthropic.com returns, captured 2026-09-21."""
from litellm.llms.anthropic.experimental_pass_through.messages import handler
safeguards = {"auto_mode": {"enabled": True, "version": "2026-09-01"}}
client_betas = "safeguards-2026-09-01,interleaved-thinking-2025-05-14"
safeguards = [{"type": "dangerous_tool_use", "classifier_context": {"v": 1, "permission_mode": "auto"}}]
client_betas = "dangerous-tool-use-2026-09-03,interleaved-thinking-2025-05-14"
safeguard_results = [{"type": "dangerous_tool_use", "status": {"type": "available", "tool_uses": {}}}]
captured: dict[str, object] = {}
def upstream_records_the_request(request: httpx.Request) -> httpx.Response:
@ -1462,7 +1464,7 @@ async def test_anthropic_messages_forwards_safeguards_and_unknown_beta_to_anthro
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {"input_tokens": 1, "output_tokens": 1},
"safeguard_results": {"verdict": "allow"},
"safeguard_results": safeguard_results,
},
request=request,
)
@ -1483,15 +1485,17 @@ async def test_anthropic_messages_forwards_safeguards_and_unknown_beta_to_anthro
assert captured["body"]["safeguards"] == safeguards
assert set(captured["anthropic-beta"].split(",")) == set(client_betas.split(","))
assert response["safeguard_results"] == {"verdict": "allow"}
assert response["safeguard_results"] == safeguard_results
@pytest.mark.asyncio
async def test_anthropic_messages_streaming_forwards_safeguards_and_keeps_safeguard_results():
"""Shapes are what Claude Code 2.1.278 sends and api.anthropic.com returns, captured 2026-09-21."""
from litellm.llms.anthropic.experimental_pass_through.messages import handler
safeguards = {"auto_mode": {"enabled": True, "version": "2026-09-01"}}
safeguard_results = {"verdict": "allow", "checks": ["shell_command"]}
safeguards = [{"type": "dangerous_tool_use", "classifier_context": {"v": 1, "permission_mode": "auto"}}]
tool_verdicts = {"toolu_01": {"type": "evaluated", "outcome": "not_flagged"}}
safeguard_results = [{"type": "dangerous_tool_use", "status": {"type": "available", "tool_uses": tool_verdicts}}]
captured: dict[str, object] = {}
message_start = {
"type": "message_start",