From 49de19e6e349048532d13eed5a62306bc376363d Mon Sep 17 00:00:00 2001 From: Sangsiva <101570025+Sangsiva@users.noreply.github.com> Date: Sat, 26 Sep 2026 00:20:56 +0800 Subject: [PATCH] fix(anthropic): don't mutate read-only requests and tighten system list typing - copy the request before stripping the Claude Code identity so a read-only wire-body mapping (shadow evaluation) no longer raises TypeError on pop/assign - annotate the system-list helper as list[object] instead of bare list - drop a comment that restated the equality check it precedes - add a regression test for the read-only request path --- litellm/llms/anthropic/common_utils.py | 5 ++-- .../adapters/transformation.py | 12 ++++++---- .../messages/transformation.py | 2 +- ...al_pass_through_adapters_transformation.py | 24 +++++++++++++++++++ 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 27aed50720a..a7b8ce2a0e1 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -143,7 +143,9 @@ def strip_claude_code_identity(text: str) -> str | None: return text -def strip_claude_code_identity_from_system(system_param: str | list | None) -> str | list | None: +def strip_claude_code_identity_from_system( + system_param: str | list[object] | None, +) -> str | list[object] | None: """Strip Claude Code's self-identification sentence from a full system parameter. Unlike :func:`strip_claude_code_identity`, which operates on a single text @@ -169,7 +171,6 @@ def strip_claude_code_identity_from_system(system_param: str | list | None) -> s stripped_text = strip_claude_code_identity(text) if stripped_text is None: continue - # Only copy the block when the text changed. if stripped_text == text: filtered_list.append(content_block) else: diff --git a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py index a0be08c4bb9..bc3dc685539 100644 --- a/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py @@ -1239,10 +1239,14 @@ class LiteLLMAnthropicMessagesAdapter: system_param: Final = anthropic_message_request.get("system") if system_param is not None: stripped_system = strip_claude_code_identity_from_system(system_param) - if stripped_system is None: - anthropic_message_request.pop("system", None) - elif stripped_system != system_param: - anthropic_message_request["system"] = stripped_system + if stripped_system is None or stripped_system != system_param: + # This adapter is also invoked with a read-only wire-body mapping + # (shadow evaluation); mutate a copy, never the caller's request. + anthropic_message_request = cast(AnthropicMessagesRequest, dict(anthropic_message_request)) + if stripped_system is None: + anthropic_message_request.pop("system", None) + else: + anthropic_message_request["system"] = stripped_system ## CONVERT ANTHROPIC MESSAGES TO OPENAI messages_list: Final[list[AllAnthropicPassThroughMessageValues]] = cast( diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py index 56bcd351ef0..f692194f0ad 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/transformation.py @@ -143,7 +143,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): return False @staticmethod - def _strip_claude_code_identity_from_system(system_param) -> str | list | None: + def _strip_claude_code_identity_from_system(system_param: str | list[object] | None) -> str | list[object] | None: """ Strip Claude Code's self-identification sentence from system parameter. diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py index e99ae2358d6..b07f65088e0 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py @@ -2900,6 +2900,30 @@ def test_translate_anthropic_to_openai_strips_claude_code_identity(system, expec assert system_messages[0]["content"] == expected_system_content +def test_translate_anthropic_to_openai_strips_claude_code_identity_read_only_request(): + """Identity stripping must not mutate a read-only request (shadow evaluation passes a + MappingProxyType, so the adapter must copy before rewriting ``system``).""" + from types import MappingProxyType + + adapter = LiteLLMAnthropicMessagesAdapter() + request = MappingProxyType( + { + "model": "hosted_vllm/kimi-k3", + "max_tokens": 1024, + "messages": [{"role": "user", "content": "hi"}], + "system": f"{CLAUDE_CODE_IDENTITY}\nYou are an interactive agent.", + } + ) + + openai_request, _ = adapter.translate_anthropic_to_openai( + anthropic_message_request=request, + custom_llm_provider="hosted_vllm", + ) + + system_messages = [m for m in openai_request["messages"] if m["role"] == "system"] + assert [m["content"] for m in system_messages] == ["You are an interactive agent."] + + def test_translate_openai_content_to_anthropic_reasoning_content_without_thinking_blocks(): """ Test that reasoning_content is converted to thinking block when thinking_blocks is not present.