Merge branch 'pr-772' into devin/1785108025-claude-prompt-caching

This commit is contained in:
Devin AI
2026-07-26 23:20:26 +00:00
2 changed files with 251 additions and 0 deletions
+104
View File
@@ -55,6 +55,110 @@ def test_child_initial_input_no_consecutive_same_role(parent_history: list[Any])
assert all(prev != nxt for prev, nxt in pairwise(roles))
def _cache_points(model_name: str) -> Any:
extra = make_model_settings(None, model_name=model_name).extra_args or {}
return extra.get("cache_control_injection_points")
@pytest.mark.parametrize(
"model_name",
[
"bedrock/global.anthropic.claude-opus-4-8",
"anthropic/claude-sonnet-4-5",
"openrouter/anthropic/claude-3.5-sonnet",
],
)
def test_make_model_settings_enables_prompt_cache_for_claude(model_name: str) -> None:
points = _cache_points(model_name)
assert points == [
{"location": "message", "role": "system"},
{"location": "tool_config"},
{"location": "message", "index": -1},
]
@pytest.mark.parametrize("model_name", ["gpt-5", "vertex_ai/gemini-2.5-pro", "openai/o3"])
def test_make_model_settings_no_prompt_cache_for_non_claude(model_name: str) -> None:
# No injection points for non-Claude models: the LiteLLM cache hook never
# fires, so this stays a strict no-op (won't emit cache_control to strict
# OpenAI-compatible endpoints).
assert make_model_settings(None, model_name=model_name).extra_args is None
def test_no_prompt_cache_for_unmapped_bedrock_claude_model(monkeypatch: Any) -> None:
"""A BEDROCK Claude route LiteLLM has NOT mapped (a new release, or any model
when LiteLLM can't refresh its model map and falls back to a stale local
copy) must run UNCACHED, not crash. Bedrock's Converse API rejects the
unknown field outright (ValidationException 'cache_control_injection_points:
Extra inputs are not permitted'); LiteLLM only strips the marker for models
it recognises as cache-capable, so an unmapped model would 500 the first
call and fail the whole run."""
import litellm
unmapped = "bedrock/global.anthropic.claude-brand-new-9"
# Simulate a model LiteLLM doesn't know: no cost-map entry, checker says no.
monkeypatch.setattr(litellm, "model_cost", {}, raising=False)
if getattr(getattr(litellm, "utils", None), "supports_prompt_caching", None):
monkeypatch.setattr(litellm.utils, "supports_prompt_caching", lambda *_a, **_k: False)
# Bedrock Claude by name, but unmapped → no injection points, no crash.
assert make_model_settings(None, model_name=unmapped).extra_args is None
def test_prompt_cache_kept_for_non_bedrock_claude_even_if_unmapped(monkeypatch: Any) -> None:
"""Non-Bedrock Claude routes must KEEP caching-by-family even when LiteLLM
can't confirm support — those providers tolerate/ignore the marker (or
LiteLLM maps them under keys we don't resolve, e.g. OpenRouter), so gating
them on confirmed support would DISABLE caching for capable models — a
regression. Only Bedrock hard-rejects, so only Bedrock is guarded."""
import litellm
monkeypatch.setattr(litellm, "model_cost", {}, raising=False)
if getattr(getattr(litellm, "utils", None), "supports_prompt_caching", None):
monkeypatch.setattr(litellm.utils, "supports_prompt_caching", lambda *_a, **_k: False)
# Even with LiteLLM knowing nothing, an Anthropic-native / OpenRouter Claude
# still gets the injection points.
for model in ("anthropic/claude-brand-new-9", "openrouter/anthropic/claude-brand-new"):
assert _cache_points(model) == [
{"location": "message", "role": "system"},
{"location": "tool_config"},
{"location": "message", "index": -1},
]
def test_conversation_tail_breakpoint_moves_with_appended_transcript() -> None:
"""The tail breakpoint's premise, end-to-end: LiteLLM's own message-injection
logic must place the cache_control on the LAST message for both a short and a
long transcript — i.e. it tracks the growing (append-only) tail rather than a
fixed position — so the immutable prefix-so-far is cached and re-read next
turn.
Driven through the hook's static ``_apply_message_injections`` primitive
(stable across LiteLLM versions) rather than the prompt-manager entrypoint
(whose signature drifts).
"""
hook_mod = pytest.importorskip("litellm.integrations.anthropic_cache_control_hook")
apply = hook_mod.AnthropicCacheControlHook._apply_message_injections
points = _cache_points("bedrock/global.anthropic.claude-opus-4-8")
msg_points = [p for p in points if p.get("location") == "message"]
def last_msg_cache_control(n_turns: int) -> Any:
messages: list[dict[str, Any]] = [{"role": "system", "content": "stable prompt"}]
for i in range(n_turns):
messages.append({"role": "assistant", "content": f"turn {i} action"})
messages.append({"role": "user", "content": f"turn {i} tool result"})
processed = apply(msg_points, messages, 4)
last = processed[-1]
content = last.get("content")
if isinstance(content, list):
return content[-1].get("cache_control")
return last.get("cache_control")
assert last_msg_cache_control(2) == {"type": "ephemeral"}
assert last_msg_cache_control(20) == {"type": "ephemeral"}
def test_build_root_task_empty_config() -> None:
assert build_root_task({}) == ""