From eb1b595068ffcd8a98bce9347c66fb9c61fb3023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?BlueT=20-=20Matthew=20Lien=20-=20=E7=B7=B4=E5=96=86?= =?UTF-8?q?=E6=98=8E?= Date: Sat, 17 Oct 2026 23:38:03 +0800 Subject: [PATCH] fix(anthropic): order messages after system and tools in /v1/messages request body The /v1/messages transform built the upstream body with messages as the first key, so the part that changes every turn led the request and the stable system and tools prefix followed it. On Vertex AI that ordering makes follow-up turns miss the prompt cache much more often. #36079 moves messages last for the chat completions path, and this applies the same ordering to the Anthropic Messages path, which Vertex AI inherits --- .../pass_through/messages/transformation.py | 6 +++--- ...al_pass_through_messages_transformation.py | 18 ++++++++++++++++++ ...artner_models_anthropic_messages_config.py | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/litellm/llms/anthropic/pass_through/messages/transformation.py b/litellm/llms/anthropic/pass_through/messages/transformation.py index 0e8c1b49e8e..f5a80e664c7 100644 --- a/litellm/llms/anthropic/pass_through/messages/transformation.py +++ b/litellm/llms/anthropic/pass_through/messages/transformation.py @@ -693,12 +693,12 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig): messages = strip_advisor_blocks_from_messages(messages) anthropic_messages_request: Final[AnthropicMessagesRequest] = AnthropicMessagesRequest( - messages=strip_encrypted_reasoning_blocks_from_anthropic_messages( - drop_non_string_tool_result_texts(messages, getattr(litellm_params, "drop_params", None)) - ), max_tokens=max_tokens, model=model, **anthropic_messages_optional_request_params, + messages=strip_encrypted_reasoning_blocks_from_anthropic_messages( + drop_non_string_tool_result_texts(messages, getattr(litellm_params, "drop_params", None)) + ), ) return dict(anthropic_messages_request) diff --git a/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_experimental_pass_through_messages_transformation.py b/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_experimental_pass_through_messages_transformation.py index 7fbec3ba12e..380219ac647 100644 --- a/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_experimental_pass_through_messages_transformation.py +++ b/tests/unit/llms/anthropic/pass_through/messages/test_anthropic_experimental_pass_through_messages_transformation.py @@ -364,3 +364,21 @@ def test_drop_params_forwards_a_non_object_message_item_as_sent_on_the_native_re assert request["messages"][1] == 7 assert request["messages"][2]["content"][0]["content"] == [{"type": "text", "text": "sunny"}] + + +def test_messages_request_body_ends_with_messages_after_system_and_tools() -> None: + messages: Final = [{"role": "user", "content": "Hello"}] + request: Final = AnthropicMessagesConfig().transform_anthropic_messages_request( + model="claude-opus-5-5", + messages=messages, + anthropic_messages_optional_request_params={ + "max_tokens": 256, + "system": [{"type": "text", "text": "You are terse."}], + "tools": [{"name": "get_weather", "input_schema": {"type": "object", "properties": {}}}], + }, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert list(request)[-1] == "messages", f"key order sent upstream: {list(request)}" + assert request["messages"] == messages diff --git a/tests/unit/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_messages_config.py b/tests/unit/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_messages_config.py index 113ddcfe2a9..0c4e80b495e 100644 --- a/tests/unit/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_messages_config.py +++ b/tests/unit/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_messages_config.py @@ -784,3 +784,22 @@ def test_vertex_claude_4_8_plus_cost_map_entries_carry_mid_conversation_system_f and info.get("supports_mid_conversation_system") is not True ] assert missing == [] + + +def test_vertex_messages_request_body_puts_system_and_tools_before_messages() -> None: + result: Final = VertexAIPartnerModelsAnthropicMessagesConfig().transform_anthropic_messages_request( + model="claude-opus-5-5", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params={ + "max_tokens": 256, + "system": [{"type": "text", "text": "You are terse."}], + "tools": [{"name": "get_weather", "input_schema": {"type": "object", "properties": {}}}], + }, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + keys: Final = list(result) + assert keys.index("system") < keys.index("messages"), f"key order sent to Vertex: {keys}" + assert keys.index("tools") < keys.index("messages"), f"key order sent to Vertex: {keys}" + assert result["messages"] == [{"role": "user", "content": "Hello"}]