|
6 | 6 |
|
7 | 7 | import strands |
8 | 8 | from strands import Agent |
9 | | -from strands.models import BedrockModel |
| 9 | +from strands.models import BedrockModel, CacheConfig, CacheToolsConfig |
10 | 10 | from strands.types.content import ContentBlock |
11 | 11 |
|
| 12 | +# Model ID used for prompt-caching TTL integration tests. Per |
| 13 | +# https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html |
| 14 | +# the models that officially support 1h TTL on CachePoint are Claude Opus 4.5, |
| 15 | +# Claude Haiku 4.5, and Claude Sonnet 4.5. Haiku 4.5 is the newest Haiku |
| 16 | +# available and is preferred for CI due to lower latency and cost relative to |
| 17 | +# the same-version Sonnet 4.5. Bump this when a newer Haiku is released that |
| 18 | +# supports CachePoint TTL. |
| 19 | +_CACHE_TTL_MODEL_ID = "us.anthropic.claude-haiku-4-5-20251001-v1:0" |
| 20 | + |
12 | 21 |
|
13 | 22 | @pytest.fixture |
14 | 23 | def system_prompt(): |
@@ -561,3 +570,127 @@ def calculator(expression: str) -> float: |
561 | 570 | agent('Search for "python" with tags ["programming", "language"] using the search tool.') |
562 | 571 |
|
563 | 572 | assert "search" in tools_called |
| 573 | + |
| 574 | + |
| 575 | +def test_prompt_caching_cache_tools_ttl(): |
| 576 | + """Test that CacheToolsConfig(ttl=...) propagates into the auto-injected toolConfig cache point. |
| 577 | +
|
| 578 | + Verifies that BedrockModel(cache_tools=CacheToolsConfig(type="default", ttl="5m")) produces a |
| 579 | + Bedrock request with cachePoint.ttl on the toolConfig checkpoint, and that the call |
| 580 | + completes without a ValidationException on the TTL field. |
| 581 | +
|
| 582 | + Note: we intentionally do not assert specific cacheWriteInputTokens on the toolConfig |
| 583 | + prefix because Bedrock's tool-prefix cache threshold varies by model and region. |
| 584 | + The critical behavior under test here is that the TTL field is accepted end-to-end. |
| 585 | +
|
| 586 | + Uses Claude Haiku 4.5 which supports TTL in CachePointBlock on Bedrock per |
| 587 | + https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html |
| 588 | + (Claude Opus 4.5, Claude Haiku 4.5, and Claude Sonnet 4.5 all support 1h TTL). |
| 589 | + """ |
| 590 | + model = BedrockModel( |
| 591 | + model_id=_CACHE_TTL_MODEL_ID, |
| 592 | + streaming=False, |
| 593 | + cache_tools=CacheToolsConfig(type="default", ttl="5m"), |
| 594 | + ) |
| 595 | + |
| 596 | + @strands.tool |
| 597 | + def lookup_fact(topic: str) -> str: |
| 598 | + """Look up a fact about the given topic. |
| 599 | +
|
| 600 | + This tool is useful when you need authoritative information. |
| 601 | + """ |
| 602 | + return f"Fact about {topic}: example" |
| 603 | + |
| 604 | + agent = Agent( |
| 605 | + model=model, |
| 606 | + tools=[lookup_fact], |
| 607 | + load_tools_from_directory=False, |
| 608 | + ) |
| 609 | + |
| 610 | + # The call must succeed — Bedrock must accept cachePoint.ttl on the toolConfig checkpoint |
| 611 | + # without raising a ValidationException. |
| 612 | + result = agent("Use the lookup_fact tool to look up 'python'.") |
| 613 | + assert len(str(result)) > 0 |
| 614 | + |
| 615 | + |
| 616 | +def test_prompt_caching_cache_config_auto_with_ttl(): |
| 617 | + """Test that CacheConfig(strategy="auto", ttl="5m") propagates TTL to the auto-injected message cache point. |
| 618 | +
|
| 619 | + Verifies that the cache point appended to the last user message by _inject_cache_point |
| 620 | + carries the configured TTL, and that Bedrock accepts the request. |
| 621 | +
|
| 622 | + Uses Claude Haiku 4.5 which supports TTL in CachePointBlock on Bedrock per |
| 623 | + https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html |
| 624 | + """ |
| 625 | + model = BedrockModel( |
| 626 | + model_id=_CACHE_TTL_MODEL_ID, |
| 627 | + streaming=False, |
| 628 | + cache_config=CacheConfig(strategy="auto", ttl="5m"), |
| 629 | + ) |
| 630 | + |
| 631 | + unique_id = str(uuid.uuid4()) |
| 632 | + # Minimum 4096 tokens required for caching with Haiku 4.5 |
| 633 | + large_message = f"Context for test {unique_id}: " + ("This is important context. " * 1000) + " What is 2+2?" |
| 634 | + |
| 635 | + agent = Agent( |
| 636 | + model=model, |
| 637 | + load_tools_from_directory=False, |
| 638 | + ) |
| 639 | + |
| 640 | + # First call: auto-injected cache point on the last user message must include ttl and be accepted |
| 641 | + result1 = agent(large_message) |
| 642 | + assert len(str(result1)) > 0 |
| 643 | + |
| 644 | + # Verify cache write occurred with auto-inject + ttl |
| 645 | + assert result1.metrics.accumulated_usage.get("cacheWriteInputTokens", 0) > 0, ( |
| 646 | + "Expected cacheWriteInputTokens > 0 with CacheConfig(strategy='auto', ttl='5m')" |
| 647 | + ) |
| 648 | + |
| 649 | + |
| 650 | +def test_prompt_caching_aligned_1h_ttl_across_checkpoints(): |
| 651 | + """Regression test for Bedrock TTL non-increasing ordering rule (Issue #2121). |
| 652 | +
|
| 653 | + Bedrock processes cache checkpoints in order: toolConfig -> system -> messages, |
| 654 | + and requires TTLs to be non-increasing. Before this change, cache_tools hardcoded |
| 655 | + an implicit 5m TTL, so any 1h TTL on a later checkpoint would raise a |
| 656 | + ValidationException. |
| 657 | +
|
| 658 | + This test sets 1h TTL on all three checkpoints simultaneously and verifies the |
| 659 | + call succeeds. |
| 660 | +
|
| 661 | + Uses Claude Haiku 4.5 which supports 1h TTL per |
| 662 | + https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html |
| 663 | + """ |
| 664 | + model = BedrockModel( |
| 665 | + model_id=_CACHE_TTL_MODEL_ID, |
| 666 | + streaming=False, |
| 667 | + cache_tools=CacheToolsConfig(type="default", ttl="1h"), |
| 668 | + cache_config=CacheConfig(strategy="auto", ttl="1h"), |
| 669 | + ) |
| 670 | + |
| 671 | + # Timestamp-based uniqueness to avoid cache conflicts across CI runs |
| 672 | + unique_id = str(int(time.time() * 1000000)) |
| 673 | + large_context = f"Background context for test {unique_id}: " + ("This is important context. " * 1000) |
| 674 | + |
| 675 | + # User-supplied 1h cache point on system prompt — third checkpoint also at 1h |
| 676 | + system_prompt_with_cache = [ |
| 677 | + {"text": large_context}, |
| 678 | + {"cachePoint": {"type": "default", "ttl": "1h"}}, |
| 679 | + {"text": "You are a helpful assistant."}, |
| 680 | + ] |
| 681 | + |
| 682 | + @strands.tool |
| 683 | + def echo(value: str) -> str: |
| 684 | + """Echo the given value back.""" |
| 685 | + return value |
| 686 | + |
| 687 | + agent = Agent( |
| 688 | + model=model, |
| 689 | + system_prompt=system_prompt_with_cache, |
| 690 | + tools=[echo], |
| 691 | + load_tools_from_directory=False, |
| 692 | + ) |
| 693 | + |
| 694 | + # Must succeed without ValidationException on the non-increasing TTL rule |
| 695 | + result = agent("What is 2+2?") |
| 696 | + assert len(str(result)) > 0 |
0 commit comments