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
This commit is contained in:
Sangsiva 2026-09-26 00:20:56 +08:00
parent b1d6264a03
commit 49de19e6e3
4 changed files with 36 additions and 7 deletions

View file

@ -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:

View file

@ -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(

View file

@ -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.

View file

@ -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.