From 2999b7adbde144edc06850cdc4b3faee97d6c92f Mon Sep 17 00:00:00 2001 From: Elliott Jacobsen-Watts Date: Fri, 17 Apr 2026 14:47:35 -0700 Subject: [PATCH 1/5] fix(litellm): forward ttl field from CachePoint in _format_system_messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LiteLLMModel._format_system_messages() was silently dropping the `ttl` field from CachePoint blocks, always emitting `{"type": "ephemeral"}`. This meant CachePoint(type="default", ttl="1h") had no effect when using LiteLLM with Anthropic-compatible endpoints (e.g. Databricks → Bedrock). Now forwards `ttl` when present, falling back to the existing behavior when absent — fully backward compatible. Closes #1721 Co-Authored-By: Claude Opus 4.7 --- src/strands/models/litellm.py | 5 ++++- tests/strands/models/test_litellm.py | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/strands/models/litellm.py b/src/strands/models/litellm.py index 36bdb5a05..853ad148e 100644 --- a/src/strands/models/litellm.py +++ b/src/strands/models/litellm.py @@ -224,7 +224,10 @@ def _format_system_messages( # Apply cache control to the immediately preceding content block # for LiteLLM/Anthropic compatibility if system_content: - system_content[-1]["cache_control"] = {"type": "ephemeral"} + cache_control: dict[str, Any] = {"type": "ephemeral"} + if ttl := block["cachePoint"].get("ttl"): + cache_control["ttl"] = ttl + system_content[-1]["cache_control"] = cache_control # Create single system message with content array rather than mulitple system messages return [{"role": "system", "content": system_content}] if system_content else [] diff --git a/tests/strands/models/test_litellm.py b/tests/strands/models/test_litellm.py index d35a1806e..c6315e557 100644 --- a/tests/strands/models/test_litellm.py +++ b/tests/strands/models/test_litellm.py @@ -955,6 +955,29 @@ def test_format_request_message_tool_call_no_reasoning_signature(): assert "__thought__" not in result["id"] +def test_format_system_messages_preserves_cache_point_ttl(): + """CachePoint with ttl="1h" should produce cache_control with ttl field.""" + result = LiteLLMModel._format_system_messages( + system_prompt_content=[ + {"text": "You are a helpful assistant."}, + {"cachePoint": {"type": "default", "ttl": "1h"}}, + ] + ) + assert result[0]["content"][0]["cache_control"] == {"type": "ephemeral", "ttl": "1h"} + + +def test_format_system_messages_cache_point_without_ttl(): + """CachePoint without ttl should produce cache_control with no ttl key (backward compat).""" + result = LiteLLMModel._format_system_messages( + system_prompt_content=[ + {"text": "You are a helpful assistant."}, + {"cachePoint": {"type": "default"}}, + ] + ) + assert result[0]["content"][0]["cache_control"] == {"type": "ephemeral"} + assert "ttl" not in result[0]["content"][0]["cache_control"] + + def test_thought_signature_round_trip(): """Test that thought signature is preserved through a full response -> internal -> request cycle.""" model = LiteLLMModel(model_id="test") From 45b1db639e3e02cbeabc2c3895cac668b552e0ec Mon Sep 17 00:00:00 2001 From: Elliott Jacobsen-Watts Date: Tue, 21 Apr 2026 12:08:33 -0700 Subject: [PATCH 2/5] fix(types): add optional ttl field to CachePoint TypedDict Keeps the type system in sync with runtime behavior now that LiteLLMModel._format_system_messages() reads and forwards ttl. Co-Authored-By: Claude Opus 4.7 --- src/strands/models/litellm.py | 2 +- src/strands/types/content.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/strands/models/litellm.py b/src/strands/models/litellm.py index 853ad148e..fb8b0bcdd 100644 --- a/src/strands/models/litellm.py +++ b/src/strands/models/litellm.py @@ -220,7 +220,7 @@ def _format_system_messages( for block in system_prompt_content or []: if "text" in block: system_content.append({"type": "text", "text": block["text"]}) - elif "cachePoint" in block and block["cachePoint"].get("type") == "default": + elif "cachePoint" in block and block["cachePoint"]["type"] == "default": # Apply cache control to the immediately preceding content block # for LiteLLM/Anthropic compatibility if system_content: diff --git a/src/strands/types/content.py b/src/strands/types/content.py index 8db1d1d98..93bfde75a 100644 --- a/src/strands/types/content.py +++ b/src/strands/types/content.py @@ -8,7 +8,7 @@ from typing import Any, Literal -from typing_extensions import NotRequired, TypedDict +from typing_extensions import NotRequired, Required, TypedDict from .citations import CitationsContentBlock from .event_loop import Metrics, Usage @@ -62,14 +62,17 @@ class ReasoningContentBlock(TypedDict, total=False): redactedContent: bytes -class CachePoint(TypedDict): +class CachePoint(TypedDict, total=False): """A cache point configuration for optimizing conversation history. Attributes: type: The type of cache point, typically "default". + ttl: Optional cache TTL duration (e.g. "5m", "1h"). Supported by providers + that accept Anthropic-compatible cache_control fields. """ - type: str + type: Required[str] + ttl: str class ContentBlock(TypedDict, total=False): From a8cd76ef954f34a9985724bee4acacfdead898c7 Mon Sep 17 00:00:00 2001 From: Elliott Jacobsen-Watts Date: Tue, 21 Apr 2026 14:10:21 -0700 Subject: [PATCH 3/5] test(litellm): cover cache_point with no preceding content block Co-Authored-By: Claude Opus 4.7 --- tests/strands/models/test_litellm.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/strands/models/test_litellm.py b/tests/strands/models/test_litellm.py index c6315e557..96cf561cd 100644 --- a/tests/strands/models/test_litellm.py +++ b/tests/strands/models/test_litellm.py @@ -978,6 +978,16 @@ def test_format_system_messages_cache_point_without_ttl(): assert "ttl" not in result[0]["content"][0]["cache_control"] +def test_format_system_messages_cache_point_with_no_preceding_content(): + """CachePoint with no preceding text block should be silently ignored.""" + result = LiteLLMModel._format_system_messages( + system_prompt_content=[ + {"cachePoint": {"type": "default", "ttl": "1h"}}, + ] + ) + assert result == [] + + def test_thought_signature_round_trip(): """Test that thought signature is preserved through a full response -> internal -> request cycle.""" model = LiteLLMModel(model_id="test") From 256b4476cda1c0c6c82d38782cd2dcbba0242869 Mon Sep 17 00:00:00 2001 From: Elliott <57333066+ElliottJW@users.noreply.github.com> Date: Thu, 23 Apr 2026 08:51:00 -0500 Subject: [PATCH 4/5] Update content.py Added NotRequired for ttl --- src/strands/types/content.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/strands/types/content.py b/src/strands/types/content.py index 93bfde75a..c0db5fdc7 100644 --- a/src/strands/types/content.py +++ b/src/strands/types/content.py @@ -62,7 +62,7 @@ class ReasoningContentBlock(TypedDict, total=False): redactedContent: bytes -class CachePoint(TypedDict, total=False): +class CachePoint(TypedDict): """A cache point configuration for optimizing conversation history. Attributes: @@ -72,7 +72,7 @@ class CachePoint(TypedDict, total=False): """ type: Required[str] - ttl: str + ttl: NotRequired[str] class ContentBlock(TypedDict, total=False): From e47e942f20375450b6d026fced86f61ebad4aba1 Mon Sep 17 00:00:00 2001 From: Elliott <57333066+ElliottJW@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:07:44 -0500 Subject: [PATCH 5/5] Update content.py Remove Required --- src/strands/types/content.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/strands/types/content.py b/src/strands/types/content.py index c0db5fdc7..5f9cc1460 100644 --- a/src/strands/types/content.py +++ b/src/strands/types/content.py @@ -8,7 +8,7 @@ from typing import Any, Literal -from typing_extensions import NotRequired, Required, TypedDict +from typing_extensions import NotRequired, TypedDict from .citations import CitationsContentBlock from .event_loop import Metrics, Usage @@ -71,7 +71,7 @@ class CachePoint(TypedDict): that accept Anthropic-compatible cache_control fields. """ - type: Required[str] + type: str ttl: NotRequired[str]