test(anthropic): cover the cache_control rebuild shapes and type the test helpers

Codecov flagged the 5m ttl branch and the empty-system path of the wire
builder; both now have a test. Greptile asked for full typing on the new
test helpers.
This commit is contained in:
Shifat Islam Santo 2026-08-23 21:21:53 -05:00
parent 87591e95d9
commit 2bd7771fb9
6 changed files with 47 additions and 16 deletions

View file

@ -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"]

View file

@ -6237,7 +6237,7 @@ REMINDER_TEXT = "<system-reminder>Answer with exactly one word.</system-reminder
CACHED_SYSTEM_BLOCK = {"type": "text", "text": "You are terse.", "cache_control": {"type": "ephemeral"}}
def _chat_request(config, model, messages):
def _chat_request(config: AnthropicConfig, model: str, messages: list[dict]) -> 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

View file

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

View file

@ -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"},

View file

@ -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"},

View file

@ -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"},