diff --git a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py index cf85329aa12..6c9d274d101 100644 --- a/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py +++ b/tests/test_litellm/litellm_core_utils/prompt_templates/test_litellm_core_utils_prompt_templates_factory.py @@ -3613,3 +3613,16 @@ def test_anthropic_messages_pt_system_string_content_becomes_text_block(): assert result[1] == {"role": "system", "content": [{"type": "text", "text": "Answer in one word."}]} + +def test_anthropic_messages_pt_drops_a_system_message_with_no_text(): + """Anthropic rejects empty text blocks, so a text-less system message must + vanish rather than reach the wire as an empty system turn.""" + messages = [ + {"role": "user", "content": "First question"}, + {"role": "system", "content": ""}, + {"role": "assistant", "content": "Yes"}, + ] + + result = anthropic_messages_pt(messages=messages, model="claude-opus-4-8", llm_provider="anthropic") + + assert [m["role"] for m in result] == ["user", "assistant"] diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py index 74d2c22bdf3..cbde7c3cd79 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -6237,7 +6237,7 @@ REMINDER_TEXT = "Answer with exactly one word. dict: return config.transform_request( model=model, messages=messages, @@ -6247,7 +6247,7 @@ def _chat_request(config, model, messages): ) -def _reminder_conversation(): +def _reminder_conversation() -> list[dict]: """The shape Claude Code sends mid-session: cached system prompt, turns, a reminder right after a user turn, an assistant turn, a fresh user turn.""" return [ @@ -6261,7 +6261,7 @@ def _reminder_conversation(): ] -def _texts(message): +def _texts(message: dict) -> list[str]: return [block["text"] for block in message["content"] if block.get("type") == "text"] @@ -6418,7 +6418,7 @@ def test_chat_mid_conversation_system_keeps_earlier_turns_a_prefix_of_the_next_r assert len(later_blocks) > len(earlier_blocks) -def _role_block_pairs(messages): +def _role_block_pairs(messages: list[dict]) -> list[tuple[str, object]]: return [ (message["role"], block) for message in messages diff --git a/tests/test_litellm/llms/anthropic/test_mid_conversation_system.py b/tests/test_litellm/llms/anthropic/test_mid_conversation_system.py index f1042a2e17e..a3a6dd50a50 100644 --- a/tests/test_litellm/llms/anthropic/test_mid_conversation_system.py +++ b/tests/test_litellm/llms/anthropic/test_mid_conversation_system.py @@ -5,6 +5,8 @@ Anthropic, Vertex, Azure AI and Bedrock Invoke transformation tests; these pin the pure placement rules on the OpenAI-format message list. """ +import pytest + import litellm from litellm.llms.anthropic.mid_conversation_system import ( CONVERTED_SYSTEM_NOTE, @@ -13,11 +15,11 @@ from litellm.llms.anthropic.mid_conversation_system import ( ) -def _roles(messages): +def _roles(messages: object) -> list[str]: return [m["role"] if isinstance(m, dict) else m.role for m in messages] -def _texts(message): +def _texts(message: dict) -> list[str]: return [block["text"] for block in message["content"]] @@ -125,19 +127,35 @@ def test_unflagged_conversion_keeps_the_client_order_when_no_tool_result_follows assert _texts(placed[2]) == [CONVERTED_SYSTEM_NOTE, "reminder"] -def test_unflagged_conversion_rebuilds_cache_control_on_the_converted_block(): +@pytest.mark.parametrize( + "cache_control, expected", + [ + ({"type": "ephemeral", "ttl": "1h"}, {"type": "ephemeral", "ttl": "1h"}), + ({"type": "ephemeral", "ttl": "5m"}, {"type": "ephemeral", "ttl": "5m"}), + ({"type": "ephemeral", "ttl": "2h"}, {"type": "ephemeral"}), + ], +) +def test_unflagged_conversion_rebuilds_cache_control_on_the_converted_block(cache_control, expected): + """Only the shapes Anthropic accepts survive: ephemeral with a 5m or 1h ttl, or no ttl.""" messages = [ {"role": "user", "content": "q1"}, - {"role": "system", "content": "reminder", "cache_control": {"type": "ephemeral", "ttl": "1h"}}, + {"role": "system", "content": "reminder", "cache_control": cache_control}, ] placed = place_mid_conversation_system(messages, supports_mid_conversation_system=False) - assert placed[1]["content"][1] == { - "type": "text", - "text": "reminder", - "cache_control": {"type": "ephemeral", "ttl": "1h"}, - } + assert placed[1]["content"][1] == {"type": "text", "text": "reminder", "cache_control": expected} + + +def test_unflagged_conversion_drops_a_cache_control_that_is_not_ephemeral(): + messages = [ + {"role": "user", "content": "q1"}, + {"role": "system", "content": "reminder", "cache_control": {"type": "persistent"}}, + ] + + placed = place_mid_conversation_system(messages, supports_mid_conversation_system=False) + + assert placed[1]["content"][1] == {"type": "text", "text": "reminder"} def test_placement_is_a_no_op_without_later_system_messages(): diff --git a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py index 257d5f92ee5..234eeae8722 100644 --- a/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py +++ b/tests/test_litellm/llms/azure_ai/claude/test_azure_anthropic_transformation.py @@ -415,7 +415,7 @@ class TestAzureAnthropicConfig: assert "context-management-2025-06-27" in headers["anthropic-beta"] -def _mid_conversation_system_conversation(): +def _mid_conversation_system_conversation() -> list[dict]: return [ {"role": "system", "content": [{"type": "text", "text": "You are terse.", "cache_control": {"type": "ephemeral"}}]}, {"role": "user", "content": "First question"}, diff --git a/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py b/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py index 080050c7621..62313c4f562 100644 --- a/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py @@ -544,7 +544,7 @@ def test_output_format_removed_from_bedrock_invoke_request(): ), f"output_format should be removed for Bedrock Invoke, got keys: {result.keys()}" -def _mid_conversation_system_conversation(): +def _mid_conversation_system_conversation() -> list[dict]: return [ {"role": "system", "content": [{"type": "text", "text": "You are terse.", "cache_control": {"type": "ephemeral"}}]}, {"role": "user", "content": "First question"}, diff --git a/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py b/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py index 65a77364bc7..d8f8f2170d7 100644 --- a/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py @@ -729,7 +729,7 @@ def test_sanitize_strips_effort_for_haiku_45(): assert data["output_config"] == {"effort": "high"} -def _mid_conversation_system_conversation(): +def _mid_conversation_system_conversation() -> list[dict]: return [ {"role": "system", "content": [{"type": "text", "text": "You are terse.", "cache_control": {"type": "ephemeral"}}]}, {"role": "user", "content": "First question"},